Преглед изворни кода

Merge remote-tracking branch 'origin/master'

吴树波 пре 2 недеља
родитељ
комит
96fcbbb086
23 измењених фајлова са 1094 додато и 520 уклоњено
  1. 47 2
      fs-admin/src/main/java/com/fs/tenant/TenantInfoController.java
  2. 24 0
      fs-common/src/main/java/com/fs/common/core/domain/TreeSelect.java
  3. 169 0
      fs-qw-api-msg/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java
  4. 218 0
      fs-qw-api-msg/src/main/java/com/fs/framework/datasource/TenantDataSourceUtil.java
  5. 2 11
      fs-qw-api/pom.xml
  6. 20 0
      fs-qw-api/src/main/java/com/fs/app/controller/OpenQwApiController.java
  7. 1 56
      fs-qw-api/src/main/java/com/fs/app/controller/QwController.java
  8. 2 0
      fs-qw-api/src/main/java/com/fs/app/service/OpenQwApiService.java
  9. 457 440
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  10. 31 0
      fs-qw-api/src/main/java/com/fs/app/service/impl/OpenQwApiServiceImpl.java
  11. 19 2
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  12. 5 0
      fs-service/src/main/java/com/fs/qw/domain/QwExternalContact.java
  13. 4 1
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  14. 3 2
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  15. 1 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  16. 5 3
      fs-service/src/main/java/com/fs/qwApi/service/impl/QwApiServiceImpl.java
  17. 3 1
      fs-service/src/main/java/com/fs/statis/impl/FsStatisQwWatchServiceImpl.java
  18. 12 0
      fs-service/src/main/java/com/fs/tenant/dto/MenuDto.java
  19. 8 0
      fs-service/src/main/java/com/fs/tenant/mapper/TenantInfoMapper.java
  20. 5 0
      fs-service/src/main/java/com/fs/tenant/service/TenantInfoService.java
  21. 20 0
      fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java
  22. 18 1
      fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml
  23. 20 0
      fs-service/src/main/resources/mapper/tenant/TenantInfoMapper.xml

+ 47 - 2
fs-admin/src/main/java/com/fs/tenant/TenantInfoController.java

@@ -8,14 +8,21 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.enums.DataSourceType;
+import com.fs.common.exception.CustomException;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.framework.datasource.TenantDataSourceManager;
 import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.dto.MenuDto;
+import com.fs.tenant.mapper.TenantInfoMapper;
 import com.fs.tenant.service.TenantInfoService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 
 /**
  * 租户基础信息Controller(SaaS 下租户表仅在主库,强制走主库)
@@ -31,6 +38,11 @@ public class TenantInfoController extends BaseController
     @Autowired
     private TenantInfoService tenantInfoService;
 
+    @Autowired
+    private TenantInfoMapper tenantInfoMapper;
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
     /**
      * 查询租户基础信息列表
      */
@@ -92,7 +104,7 @@ public class TenantInfoController extends BaseController
      * 修改租户基础信息
      */
     @PreAuthorize("@ss.hasPermi('tenant:tenant:edit')")
-    @Log(title = "租户基础信息", businessType = BusinessType.UPDATE)
+    @Log(title = "修改租户基础信息", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody TenantInfo tenantInfo)
     {
@@ -103,10 +115,43 @@ public class TenantInfoController extends BaseController
      * 删除租户基础信息
      */
     @PreAuthorize("@ss.hasPermi('tenant:tenant:remove')")
-    @Log(title = "租户基础信息", businessType = BusinessType.DELETE)
+    @Log(title = "删除租户基础信息", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable String[] ids)
     {
         return toAjax(tenantInfoService.deleteTenantInfoByIds(ids));
     }
+
+    /**
+     * 租户菜单修改(获取租户菜单)
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:edit')")
+    @GetMapping("/menu/{id}")
+    public R menuChange(@PathVariable String id)
+    {
+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoById(id);
+        if (tenantInfo.getStatus() == 2) {
+            throw new CustomException("租户初始化中");
+        }
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        return tenantInfoService.menuChange(id);
+    }
+
+    /**
+     * 租户菜单修改
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:edit')")
+    @PostMapping("/menu/edit")
+    public R menuEdit(@RequestBody MenuDto menuDto)
+    {
+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoById(menuDto.getId());
+
+        if (tenantInfo.getStatus() == 2) {
+            throw new CustomException("租户初始化中");
+        }
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        List<Long> selected = menuDto.getSelected();
+        List<Long> unSelected = menuDto.getUnSelected();
+        return tenantInfoService.menuEdit(selected, unSelected);
+    }
 }

+ 24 - 0
fs-common/src/main/java/com/fs/common/core/domain/TreeSelect.java

@@ -22,6 +22,12 @@ public class TreeSelect implements Serializable
     /** 节点名称 */
     private String label;
 
+    /** 菜单状态(0显示 1隐藏) */
+    private String visible;
+
+    /** 菜单状态(0显示 1停用) */
+    private String status;
+
     /** 子节点 */
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
     private List<TreeSelect> children;
@@ -43,6 +49,8 @@ public class TreeSelect implements Serializable
         this.id = menu.getMenuId();
         this.label = menu.getMenuName();
         this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());
+        this.visible = menu.getVisible();
+        this.status = menu.getStatus();
     }
 
     public Long getId()
@@ -60,6 +68,22 @@ public class TreeSelect implements Serializable
         return label;
     }
 
+    public String getVisible() {
+        return visible;
+    }
+
+    public void setVisible(String visible) {
+        this.visible = visible;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
     public void setLabel(String label)
     {
         this.label = label;

+ 169 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java

@@ -0,0 +1,169 @@
+package com.fs.framework.datasource;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 租户数据源管理器
+ * 负责动态创建和管理租户数据源
+ */
+@Component
+public class TenantDataSourceManager {
+
+    private static final Logger log = LoggerFactory.getLogger(TenantDataSourceManager.class);
+
+    @Resource
+    private DynamicDataSource dynamicDataSource;
+
+    @Resource
+    private TenantInfoService tenantInfoService;
+
+    /**
+     * 租户数据源缓存
+     */
+    private static final Map<String, DataSource> TENANT_DS_CACHE = new ConcurrentHashMap<>();
+
+    /**
+     * 切换到租户数据源(不存在则创建)
+     */
+    public void switchTenant(TenantInfo tenantInfo) {
+
+        // 用租户主键作为唯一标识
+        String tenantKey = buildTenantKey(tenantInfo.getId());
+
+        if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+            synchronized (this) {
+                if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+
+                    DataSource tenantDs = createTenantDataSource(tenantInfo);
+                    TENANT_DS_CACHE.put(tenantKey, tenantDs);
+
+                    // 动态追加到已解析的数据源
+                    Map<Object, DataSource> resolvedMap = getResolvedDataSources();
+                    resolvedMap.put(tenantKey, tenantDs);
+                }
+            }
+        }
+
+        // ThreadLocal 切库
+        DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+    }
+
+    private String buildTenantKey(Long tenantId) {
+        return "tenant:" + tenantId;
+    }
+
+    /**
+     * 清理 ThreadLocal
+     */
+    public void clear() {
+        DynamicDataSourceContextHolder.clearDataSourceType();
+    }
+
+    /**
+     * 根据租户ID确保数据源已注册并切换(用于 Filter/拦截器等非登录场景)
+     * 解决 JVM 重启后 TENANT_DS_CACHE 被清空,导致 resolvedDataSources 中找不到租户数据源的问题
+     *
+     * @param tenantId 租户ID
+     */
+    public void ensureSwitchByTenantId(Long tenantId) {
+        String tenantKey = buildTenantKey(tenantId);
+
+        // 如果缓存中已有,直接切库
+        if (TENANT_DS_CACHE.containsKey(tenantKey)) {
+            DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+            log.debug("[TenantDS] 数据源已缓存,直接切换: {}", tenantKey);
+            return;
+        }
+
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+            if (tenantInfo == null) {
+                log.warn("[TenantDS] 租户ID={} 在主库中不存在,回退到主库", tenantId);
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+                return;
+            }
+            if (!tenantInfo.getStatus().equals(1)) {
+                log.warn("[TenantDS] 租户ID={} 已禁用,回退到主库", tenantId);
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+                return;
+            }
+            switchTenant(tenantInfo);
+            log.info("[TenantDS] 动态注册并切换数据源: key={}, url={}", tenantKey, tenantInfo.getDbUrl());
+        } catch (Exception e) {
+            log.error("[TenantDS] 动态注册租户数据源失败, tenantId={}, 回退到主库", tenantId, e);
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        }
+    }
+
+    /**
+     * 获取租户数据源
+     */
+    public DataSource getTenantDataSource(Long tenantId) {
+        String tenantKey = buildTenantKey(tenantId);
+        if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+            DynamicDataSourceContextHolder.setDataSourceType("MASTER");
+            try {
+                TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+                if (tenantInfo != null && tenantInfo.getStatus().equals(1)) {
+                    DataSource tenantDs = createTenantDataSource(tenantInfo);
+                    TENANT_DS_CACHE.put(tenantKey, tenantDs);
+                    Map<Object, DataSource> resolvedMap = getResolvedDataSources();
+                    resolvedMap.put(tenantKey, tenantDs);
+                    return tenantDs;
+                }
+            } finally {
+                DynamicDataSourceContextHolder.clearDataSourceType();
+            }
+        }
+        return TENANT_DS_CACHE.get(tenantKey);
+    }
+
+    /**
+     * 创建租户数据源(MySQL + Druid)
+     */
+    private DataSource createTenantDataSource(TenantInfo tenant) {
+
+        DruidDataSource ds = new DruidDataSource();
+        ds.setUrl(tenant.getDbUrl());
+        ds.setUsername(tenant.getDbAccount());
+        ds.setPassword(tenant.getDbPwd());
+
+        // 统一 MySQL
+        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
+
+        ds.setInitialSize(5);
+        ds.setMinIdle(10);
+        ds.setMaxActive(20);
+        ds.setMaxWait(60000);
+
+        return ds;
+    }
+
+    /**
+     * 反射获取 AbstractRoutingDataSource.resolvedDataSources
+     */
+    @SuppressWarnings("unchecked")
+    private Map<Object, DataSource> getResolvedDataSources() {
+        try {
+            Field field = org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
+                    .class.getDeclaredField("resolvedDataSources");
+            field.setAccessible(true);
+            return (Map<Object, DataSource>) field.get(dynamicDataSource);
+        } catch (Exception e) {
+            throw new IllegalStateException("获取 resolvedDataSources 失败", e);
+        }
+    }
+}

+ 218 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/datasource/TenantDataSourceUtil.java

@@ -0,0 +1,218 @@
+package com.fs.framework.datasource;
+
+import com.fs.common.config.RedisTenantContext;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * 租户数据源切换工具类
+ * 用于在指定租户数据源下执行增删改查操作
+ */
+@Slf4j
+@Component
+public class TenantDataSourceUtil {
+
+    @Resource
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Resource
+    private TenantInfoService tenantInfoService;
+
+    /**
+     * 在指定租户数据源下执行操作(无返回值)
+     *
+     * @param tenantId 租户ID
+     * @param action   要执行的操作
+     */
+    public void execute(Long tenantId, Runnable action) {
+        TenantInfo tenantInfo = getTenantInfo(tenantId);
+        if (tenantInfo == null) {
+            throw new IllegalArgumentException("租户不存在或已禁用,tenantId=" + tenantId);
+        }
+        execute(tenantInfo, action);
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(无返回值)
+     *
+     * @param tenantInfo 租户信息
+     * @param action     要执行的操作
+     */
+    public void execute(TenantInfo tenantInfo, Runnable action) {
+        try {
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+            log.debug("[TenantDS] 已切换到租户数据源和Redis: tenantId={}, tenantCode={}",
+                    tenantInfo.getId(), tenantInfo.getTenantCode());
+
+            // 执行操作
+            action.run();
+
+        } finally {
+            // 清理数据源上下文
+            tenantDataSourceManager.clear();
+            // 清理Redis租户上下文
+            RedisTenantContext.clear();
+            log.debug("[TenantDS] 已清理租户数据源和Redis上下文");
+        }
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(带返回值)
+     *
+     * @param tenantId 租户ID
+     * @param action   要执行的操作
+     * @param <T>      返回值类型
+     * @return 操作结果
+     */
+    public <T> T executeWithResult(Long tenantId, Supplier<T> action) {
+        TenantInfo tenantInfo = getTenantInfo(tenantId);
+        if (tenantInfo == null) {
+            throw new IllegalArgumentException("租户不存在或已禁用,tenantId=" + tenantId);
+        }
+        return executeWithResult(tenantInfo, action);
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(带返回值)
+     *
+     * @param tenantInfo 租户信息
+     * @param action     要执行的操作
+     * @param <T>        返回值类型
+     * @return 操作结果
+     */
+    public <T> T executeWithResult(TenantInfo tenantInfo, Supplier<T> action) {
+        try {
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+            log.debug("[TenantDS] 已切换到租户数据源和Redis: tenantId={}, tenantCode={}",
+                    tenantInfo.getId(), tenantInfo.getTenantCode());
+
+            // 执行操作并返回结果
+            return action.get();
+
+        } finally {
+            // 清理数据源上下文
+            tenantDataSourceManager.clear();
+            // 清理Redis租户上下文
+            RedisTenantContext.clear();
+            log.debug("[TenantDS] 已清理租户数据源和Redis上下文");
+        }
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(消费租户信息,无返回值)
+     *
+     * @param tenantId 租户ID
+     * @param action   要执行的操作,接收租户信息作为参数
+     */
+    public void executeWithTenant(Long tenantId, Consumer<TenantInfo> action) {
+        TenantInfo tenantInfo = getTenantInfo(tenantId);
+        if (tenantInfo == null) {
+            throw new IllegalArgumentException("租户不存在或已禁用,tenantId=" + tenantId);
+        }
+        executeWithTenant(tenantInfo, action);
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(消费租户信息,无返回值)
+     *
+     * @param tenantInfo 租户信息
+     * @param action     要执行的操作,接收租户信息作为参数
+     */
+    public void executeWithTenant(TenantInfo tenantInfo, Consumer<TenantInfo> action) {
+        try {
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+            log.debug("[TenantDS] 已切换到租户数据源和Redis: tenantId={}, tenantCode={}",
+                    tenantInfo.getId(), tenantInfo.getTenantCode());
+
+            // 执行操作
+            action.accept(tenantInfo);
+
+        } finally {
+            // 清理数据源上下文
+            tenantDataSourceManager.clear();
+            // 清理Redis租户上下文
+            RedisTenantContext.clear();
+            log.debug("[TenantDS] 已清理租户数据源和Redis上下文");
+        }
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(消费租户信息,带返回值)
+     *
+     * @param tenantId 租户ID
+     * @param action   要执行的操作,接收租户信息并返回结果
+     * @param <R>      返回值类型
+     * @return 操作结果
+     */
+    public <R> R executeWithTenantAndResult(Long tenantId, Function<TenantInfo, R> action) {
+        TenantInfo tenantInfo = getTenantInfo(tenantId);
+        if (tenantInfo == null) {
+            throw new IllegalArgumentException("租户不存在或已禁用,tenantId=" + tenantId);
+        }
+        return executeWithTenantAndResult(tenantInfo, action);
+    }
+
+    /**
+     * 在指定租户数据源下执行操作(消费租户信息,带返回值)
+     *
+     * @param tenantInfo 租户信息
+     * @param action     要执行的操作,接收租户信息并返回结果
+     * @param <R>        返回值类型
+     * @return 操作结果
+     */
+    public <R> R executeWithTenantAndResult(TenantInfo tenantInfo, Function<TenantInfo, R> action) {
+        try {
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+            log.debug("[TenantDS] 已切换到租户数据源和Redis: tenantId={}, tenantCode={}",
+                    tenantInfo.getId(), tenantInfo.getTenantCode());
+
+            // 执行操作并返回结果
+            return action.apply(tenantInfo);
+
+        } finally {
+            // 清理数据源上下文
+            tenantDataSourceManager.clear();
+            // 清理Redis租户上下文
+            RedisTenantContext.clear();
+            log.debug("[TenantDS] 已清理租户数据源和Redis上下文");
+        }
+    }
+
+    /**
+     * 获取租户信息
+     *
+     * @param tenantId 租户ID
+     * @return 租户信息,不存在或已禁用则返回null
+     */
+    private TenantInfo getTenantInfo(Long tenantId) {
+        TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+        if (tenantInfo == null) {
+            log.warn("[TenantDS] 租户不存在,tenantId={}", tenantId);
+            return null;
+        }
+        if (!Integer.valueOf(1).equals(tenantInfo.getStatus())) {
+            log.warn("[TenantDS] 租户已禁用,tenantId={}", tenantId);
+            return null;
+        }
+        return tenantInfo;
+    }
+}

+ 2 - 11
fs-qw-api/pom.xml

@@ -105,12 +105,12 @@
 
     <build>
         <plugins>
-            <!--<plugin>
+            <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
                 <version>2.1.1.RELEASE</version>
                 <configuration>
-                    <fork>true</fork> &lt;!&ndash; 如果没有该配置,devtools不会生效 &ndash;&gt;
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
                 </configuration>
                 <executions>
                     <execution>
@@ -119,15 +119,6 @@
                         </goals>
                     </execution>
                 </executions>
-            </plugin>-->
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <configuration>
-                    <source>${java.version}</source>
-                    <target>${java.version}</target>
-                    <encoding>${project.build.sourceEncoding}</encoding>
-                </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>

+ 20 - 0
fs-qw-api/src/main/java/com/fs/app/controller/OpenQwApiController.java

@@ -162,5 +162,25 @@ public class OpenQwApiController extends BaseController {
             return R.error("移除标签失败: " + e.getMessage());
         }
     }
+
+    /**
+     * 加密ExternalUserid
+     */
+    @PostMapping("/getOpenExternalUserid")
+    public R getOpenExternalUserid(@RequestParam("externalUserid") String  externalUserid,
+                                   @RequestParam("corpId") String  corpId,
+                                   @RequestParam("qwUserId") String  qwUserId,
+                                   @RequestParam(value = "tenantId", required = true) Long tenantId){
+        try {
+            return tenantDataSourceUtil.executeWithResult(tenantId,
+                    () ->  openQwApiService.getOpenExternalUserid(externalUserid,corpId,qwUserId));
+        } catch (IllegalArgumentException e) {
+            log.error("加密ExternalUserid,租户不存在或已禁用,tenantId={}", tenantId, e);
+            return R.error("租户不存在或已禁用");
+        } catch (Exception e) {
+            log.error("加密ExternalUserid,tenantId={}", tenantId, e);
+            return R.error("加密ExternalUserid失败: " + e.getMessage());
+        }
+    }
 }
 

+ 1 - 56
fs-qw-api/src/main/java/com/fs/app/controller/QwController.java

@@ -29,8 +29,6 @@ public class QwController {
     @Autowired
     private QwDataCallbackService qwDataCallbackService;
 
-    @Resource
-    private TenantDataSourceManager tenantDataSourceManager;
 
     /**
      * 功能描述:
@@ -50,20 +48,11 @@ public class QwController {
         }
 
         try {
-            // 切换到租户数据源
-            tenantDataSourceManager.ensureSwitchByTenantId(qwCompany.getTenantId());
-            // 设置 Redis 租户上下文
-            RedisTenantContext.setTenantId(qwCompany.getTenantId());
-
             return getVerify(msg_signature, timestamp, nonce, echostr, corpId, qwCompany);
         } catch (Exception e) {
             logger.error("[QwCallback] 数据回调GET请求处理失败,corpId={}, tenantId={}",
                     corpId, qwCompany.getTenantId(), e);
             return "error";
-        } finally {
-            // 清理租户上下文
-            RedisTenantContext.clear();
-            tenantDataSourceManager.clear();
         }
     }
 
@@ -74,6 +63,7 @@ public class QwController {
                         @RequestParam("timestamp") String timestamp,
                         @RequestParam(value = "corpid", required = false) String corpid,
                         @RequestParam("nonce") String nonce) {
+        logger.error("corpid={}", corpId);
         QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
         if (qwCompany == null || qwCompany.getTenantId() == null) {
             logger.error("[QwCallback] 未找到企业配置或租户ID,corpId={}", corpId);
@@ -81,20 +71,11 @@ public class QwController {
         }
 
         try {
-            // 切换到租户数据源
-            tenantDataSourceManager.ensureSwitchByTenantId(qwCompany.getTenantId());
-            // 设置 Redis 租户上下文
-            RedisTenantContext.setTenantId(qwCompany.getTenantId());
-
             return dataCallback(msg_signature, timestamp, nonce, requestBody, corpId, qwCompany);
         } catch (Exception e) {
             logger.error("[QwCallback] 数据回调POST请求处理失败,corpId={}, tenantId={}",
                     corpId, qwCompany.getTenantId(), e);
             return "error";
-        } finally {
-            // 清理租户上下文
-            RedisTenantContext.clear();
-            tenantDataSourceManager.clear();
         }
     }
 
@@ -180,11 +161,6 @@ public class QwController {
         }
 
         try {
-            // 切换到租户数据源
-            tenantDataSourceManager.ensureSwitchByTenantId(qwCompany.getTenantId());
-            // 设置 Redis 租户上下文
-            RedisTenantContext.setTenantId(qwCompany.getTenantId());
-
             logger.info("[QwCallback] 数据回调URLServer-微信调用dataGet请求,corpId={}, tenantId={}",
                     corpId, qwCompany.getTenantId());
             return getVerifyServer(msg_signature, timestamp, nonce, echostr, corpId, qwCompany);
@@ -192,10 +168,6 @@ public class QwController {
             logger.error("[QwCallback] Server数据回调GET请求处理失败,corpId={}, tenantId={}",
                     corpId, qwCompany.getTenantId(), e);
             return "error";
-        } finally {
-            // 清理租户上下文
-            RedisTenantContext.clear();
-            tenantDataSourceManager.clear();
         }
     }
 
@@ -213,11 +185,6 @@ public class QwController {
         }
 
         try {
-            // 切换到租户数据源
-            tenantDataSourceManager.ensureSwitchByTenantId(qwCompany.getTenantId());
-            // 设置 Redis 租户上下文
-            RedisTenantContext.setTenantId(qwCompany.getTenantId());
-
             logger.info("[QwCallback] 数据回调URLServer-dataPost,corpId={}, tenantId={}",
                     corpId, qwCompany.getTenantId());
             return qwDataCallbackService.dataCallbackServer(msg_signature, timestamp, nonce, requestBody, corpId);
@@ -225,10 +192,6 @@ public class QwController {
             logger.error("[QwCallback] Server数据回调POST请求处理失败,corpId={}, tenantId={}",
                     corpId, qwCompany.getTenantId(), e);
             return "error";
-        } finally {
-            // 清理租户上下文
-            RedisTenantContext.clear();
-            tenantDataSourceManager.clear();
         }
     }
 
@@ -283,20 +246,11 @@ public class QwController {
         }
 
         try {
-            // 切换到租户数据源
-            tenantDataSourceManager.ensureSwitchByTenantId(qwCompany.getTenantId());
-            // 设置 Redis 租户上下文
-            RedisTenantContext.setTenantId(qwCompany.getTenantId());
-
             return getVerifyServerAuth(msg_signature, timestamp, nonce, echostr, corpId, qwCompany);
         } catch (Exception e) {
             logger.error("[QwCallback] ServerAuth数据回调GET请求处理失败,corpId={}, tenantId={}",
                     corpId, qwCompany.getTenantId(), e);
             return "error";
-        } finally {
-            // 清理租户上下文
-            RedisTenantContext.clear();
-            tenantDataSourceManager.clear();
         }
     }
 
@@ -340,11 +294,6 @@ public class QwController {
         }
 
         try {
-            // 切换到租户数据源
-            tenantDataSourceManager.ensureSwitchByTenantId(qwCompany.getTenantId());
-            // 设置 Redis 租户上下文
-            RedisTenantContext.setTenantId(qwCompany.getTenantId());
-
             logger.info("[QwCallback] 数据回调URLServerAuth-dataPost,corpId={}, tenantId={}",
                     corpID, qwCompany.getTenantId());
             return qwDataCallbackService.dataCallbackServerAuth(msg_signature, timestamp, nonce, requestBody, corpID);
@@ -352,10 +301,6 @@ public class QwController {
             logger.error("[QwCallback] ServerAuth数据回调POST请求处理失败,corpId={}, tenantId={}",
                     corpID, qwCompany.getTenantId(), e);
             return "error";
-        } finally {
-            // 清理租户上下文
-            RedisTenantContext.clear();
-            tenantDataSourceManager.clear();
         }
     }
 }

+ 2 - 0
fs-qw-api/src/main/java/com/fs/app/service/OpenQwApiService.java

@@ -19,4 +19,6 @@ public interface OpenQwApiService {
     R addTag(QwExternalContactAddTagParam param);
 
     R delTag(QwExternalContactAddTagParam param);
+
+    R getOpenExternalUserid(String externalUserid,String corpId,String qwUserId);
 }

+ 457 - 440
fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java

@@ -4,11 +4,13 @@ import cn.hutool.core.util.RandomUtil;
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.app.util.WXBizMsgCrypt;
+import com.fs.common.config.RedisTenantContext;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.company.service.ICompanyConfigService;
+import com.fs.framework.datasource.TenantDataSourceManager;
 import com.fs.newAdv.service.ILeadService;
 import com.fs.qw.domain.*;
 import com.fs.qw.mapper.QwCompanyMapper;
@@ -243,516 +245,531 @@ public class QwDataCallbackService {
         } catch (Exception ignore) {
         }
     }
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
 
     @Async
     public void dataCallback( Document document,String corpId,QwCompany qwCompany) throws Exception {
+        try{
 
-
-        Element root = document.getDocumentElement();
-
-        // 先判断是否是 suite_ticket 类型的回调
-        NodeList infoTypeNode = root.getElementsByTagName("InfoType");
-        if (infoTypeNode.getLength() > 0) {
-            String infoType = infoTypeNode.item(0).getTextContent();
-            if ("suite_ticket".equals(infoType)) {
-                // 处理 suite_ticket 推送
-                NodeList suiteTicketNode = root.getElementsByTagName("SuiteTicket");
-                String suiteTicket = suiteTicketNode.item(0).getTextContent();
-                // 保存 suite_ticket,用于获取永久授权码等
-                String cacheKey = "qw_suite_ticket:" + corpId;
-                // 企微约每 10~30 分钟推送;适当延长避免 reset_permanent_code 等场景下 ticket 已过期
-                redisCache.setCacheObject(cacheKey, suiteTicket, 6, TimeUnit.HOURS);
-                logger.info("收到 suite_ticket: {},corpId:{}", suiteTicket,corpId);
-                return;
-            }
-            // 重置永久授权码 / 企业首次授权安装:用 AuthCode 换 permanent_code 并更新库
-            if ("reset_permanent_code".equals(infoType) || "create_auth".equals(infoType)) {
-                NodeList authCodeList = root.getElementsByTagName("AuthCode");
-                if (authCodeList.getLength() == 0) {
-                    logger.error("{} 回调缺少 AuthCode, corpId={}", infoType, corpId);
-                    return;
+                // 切换到租户数据源
+                tenantDataSourceManager.ensureSwitchByTenantId(qwCompany.getTenantId());
+                // 设置 Redis 租户上下文
+                RedisTenantContext.setTenantId(qwCompany.getTenantId());
+
+                Element root = document.getDocumentElement();
+
+                // 先判断是否是 suite_ticket 类型的回调
+                NodeList infoTypeNode = root.getElementsByTagName("InfoType");
+                if (infoTypeNode.getLength() > 0) {
+                    String infoType = infoTypeNode.item(0).getTextContent();
+                    if ("suite_ticket".equals(infoType)) {
+                        // 处理 suite_ticket 推送
+                        NodeList suiteTicketNode = root.getElementsByTagName("SuiteTicket");
+                        String suiteTicket = suiteTicketNode.item(0).getTextContent();
+                        // 保存 suite_ticket,用于获取永久授权码等
+                        String cacheKey = "qw_suite_ticket:" + corpId;
+                        // 企微约每 10~30 分钟推送;适当延长避免 reset_permanent_code 等场景下 ticket 已过期
+                        redisCache.setCacheObject(cacheKey, suiteTicket, 6, TimeUnit.HOURS);
+                        logger.info("收到 suite_ticket: {},corpId:{}", suiteTicket,corpId);
+                        return;
+                    }
+                    // 重置永久授权码 / 企业首次授权安装:用 AuthCode 换 permanent_code 并更新库
+                    if ("reset_permanent_code".equals(infoType) || "create_auth".equals(infoType)) {
+                        NodeList authCodeList = root.getElementsByTagName("AuthCode");
+                        if (authCodeList.getLength() == 0) {
+                            logger.error("{} 回调缺少 AuthCode, corpId={}", infoType, corpId);
+                            return;
+                        }
+                        String authCode = authCodeList.item(0).getTextContent();
+                        NodeList suiteIdNodes = root.getElementsByTagName("SuiteId");
+                        String suiteIdXml = suiteIdNodes.getLength() > 0 ? suiteIdNodes.item(0).getTextContent() : null;
+                        logger.info("收到 {} 回调, corpId={}, suiteId={}", infoType, corpId, suiteIdXml);
+                        syncPermanentCodeByAuthCode(qwCompany, corpId, authCode, suiteIdXml);
+                        return;
+                    }
                 }
-                String authCode = authCodeList.item(0).getTextContent();
-                NodeList suiteIdNodes = root.getElementsByTagName("SuiteId");
-                String suiteIdXml = suiteIdNodes.getLength() > 0 ? suiteIdNodes.item(0).getTextContent() : null;
-                logger.info("收到 {} 回调, corpId={}, suiteId={}", infoType, corpId, suiteIdXml);
-                syncPermanentCodeByAuthCode(qwCompany, corpId, authCode, suiteIdXml);
-                return;
-            }
-        }
-
-        NodeList authCodeNode = root.getElementsByTagName("AuthCode");
-        if (authCodeNode.getLength() > 0) {
-            String authCodeTemp = authCodeNode.item(0).getTextContent();
-            if (StringUtils.isNotBlank(authCodeTemp)) {
-                NodeList suiteIdNodes = root.getElementsByTagName("SuiteId");
-                String suiteIdXml = suiteIdNodes.getLength() > 0 ? suiteIdNodes.item(0).getTextContent() : null;
-                syncPermanentCodeByAuthCode(qwCompany, corpId, authCodeTemp, suiteIdXml);
-            }
-            return;
 
-        }
+                NodeList authCodeNode = root.getElementsByTagName("AuthCode");
+                if (authCodeNode.getLength() > 0) {
+                    String authCodeTemp = authCodeNode.item(0).getTextContent();
+                    if (StringUtils.isNotBlank(authCodeTemp)) {
+                        NodeList suiteIdNodes = root.getElementsByTagName("SuiteId");
+                        String suiteIdXml = suiteIdNodes.getLength() > 0 ? suiteIdNodes.item(0).getTextContent() : null;
+                        syncPermanentCodeByAuthCode(qwCompany, corpId, authCodeTemp, suiteIdXml);
+                    }
+                    return;
 
+                }
 
 
-        NodeList msgTypeNode = root.getElementsByTagName("MsgType");
 
-        // 如果连 MsgType 都没有,说明是其他类型回调,直接返回
-        if (msgTypeNode.getLength() == 0) {
-            logger.error("未知回调类型,无 MsgType");
-            return;
-        }
+                NodeList msgTypeNode = root.getElementsByTagName("MsgType");
 
-        String msgType = msgTypeNode.item(0).getTextContent();
-        if(msgType.equals("event")){
-            NodeList eventNode = root.getElementsByTagName("Event");
-            String event = eventNode.item(0).getTextContent();
-            String changeType ="";
-            NodeList changeTypeList = root.getElementsByTagName("ChangeType");
-            if(changeTypeList.getLength() > 0) {
-                changeType = changeTypeList.item(0).getTextContent();
-            }
+                // 如果连 MsgType 都没有,说明是其他类型回调,直接返回
+                if (msgTypeNode.getLength() == 0) {
+                    logger.error("未知回调类型,无 MsgType");
+                    return;
+                }
 
-            switch (event){
-                //调用api接口删除员工
-                case "unsubscribe" :
-                    String fromUserName = root.getElementsByTagName("FromUserName").item(0).getTextContent();
-                    QwUser delQwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, fromUserName);
-                    if (delQwUser!=null){
-                        delQwUser.setIsDel(2);
-                        delQwUser.setStatus(0);
-                        qwUserMapper.updateQwUser(delQwUser);
-                    }
-                    break;
-                case "subscribe":
-                    String fromUserNameUserId = root.getElementsByTagName("FromUserName").item(0).getTextContent();
-                    QwUser updateOrInsertQwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, fromUserNameUserId);
-                    //userid转openid
-                    QwOpenidByUserParams param=new QwOpenidByUserParams();
-                    param.setUserid(fromUserNameUserId);
-                    QwOpenidResult qwOpenidResult = qwApiService.useridToOpenid(param, corpId);
-                    if (updateOrInsertQwUser!=null&&qwOpenidResult.getErrCode()==0){
-                        updateOrInsertQwUser.setOpenid(qwOpenidResult.getOpenid());
-                        qwUserMapper.updateQwUser(updateOrInsertQwUser);
+                String msgType = msgTypeNode.item(0).getTextContent();
+                if(msgType.equals("event")){
+                    NodeList eventNode = root.getElementsByTagName("Event");
+                    String event = eventNode.item(0).getTextContent();
+                    String changeType ="";
+                    NodeList changeTypeList = root.getElementsByTagName("ChangeType");
+                    if(changeTypeList.getLength() > 0) {
+                        changeType = changeTypeList.item(0).getTextContent();
                     }
 
-                    break;
-                case "change_contact" :
-                    if (changeType.equals("create_user")){
-                        String userID = root.getElementsByTagName("UserID").item(0).getTextContent();
-                        QwUser qw = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, userID);
-                        QwUser qwUser = new QwUser();
-                        qwUser.setQwUserId(userID);
-                        qwUser.setCreateTime(new Date());
-                        qwUser.setCorpId(corpId);
-
-                        if (qw==null){
-                            String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), userID,qwCompany.getPermanentCode());
-                            qwUser.setQwUserId(userID);
-                            qwUser.setDepartment(root.getElementsByTagName("Department").item(0).getTextContent().toString());
-                            qwUser.setQwUserName(serverQwUserName);
-                            QwOpenidByUserParams param1=new QwOpenidByUserParams();
-                            param1.setUserid(userID);
-                            QwOpenidResult qwOpenid = qwApiService.useridToOpenid(param1, corpId);
-                            qwUser.setOpenid(qwOpenid.getOpenid());
-                            qwUserMapper.insertQwUser(qwUser);
-                        }else {
-                            qw.setIsDel(0);
-                            qw.setUpdateTime(new Date());
-                            qwUserMapper.updateQwUser(qw);
-                        }
-                    }
-                    else if (changeType.equals("delete_user")){
-                        String userID = root.getElementsByTagName("UserID").item(0).getTextContent();
-                        QwUser qwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, userID);
-                        if (qwUser!=null){
-                            qwUser.setIsDel(2);
-                            qwUser.setStatus(0);
-                            qwUserMapper.updateQwUser(qwUser);
-                            QwExternalContact qwExternalContact = new QwExternalContact();
-                            qwExternalContact.setUserId(userID);
-                            qwExternalContact.setStatus(0);
-                            qwExternalContact.setCorpId(corpId);
-                            List<QwExternalContact> qwExternalContacts = qwExternalContactService.selectQwExternalContactList(qwExternalContact);
-                            for (QwExternalContact externalContact : qwExternalContacts) {
-                                QwExternalContact lz = new QwExternalContact();
-                                lz.setId(externalContact.getId());
-                                lz.setStatus(1);
-                                lz.setCorpId(corpId);
-                                qwExternalContactMapper.updateQwExternalContact(lz);
+                    switch (event){
+                        //调用api接口删除员工
+                        case "unsubscribe" :
+                            String fromUserName = root.getElementsByTagName("FromUserName").item(0).getTextContent();
+                            QwUser delQwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, fromUserName);
+                            if (delQwUser!=null){
+                                delQwUser.setIsDel(2);
+                                delQwUser.setStatus(0);
+                                qwUserMapper.updateQwUser(delQwUser);
+                            }
+                            break;
+                        case "subscribe":
+                            String fromUserNameUserId = root.getElementsByTagName("FromUserName").item(0).getTextContent();
+                            QwUser updateOrInsertQwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, fromUserNameUserId);
+                            //userid转openid
+                            QwOpenidByUserParams param=new QwOpenidByUserParams();
+                            param.setUserid(fromUserNameUserId);
+                            QwOpenidResult qwOpenidResult = qwApiService.useridToOpenid(param, corpId);
+                            if (updateOrInsertQwUser!=null&&qwOpenidResult.getErrCode()==0){
+                                updateOrInsertQwUser.setOpenid(qwOpenidResult.getOpenid());
+                                qwUserMapper.updateQwUser(updateOrInsertQwUser);
                             }
-                        }
-                    }
-                    else if (changeType.equals("create_party")){
 
-                        qwDeptService.insertOrUpdateQwDept(corpId);
-                    }
-                    else if (changeType.equals("update_party")){
+                            break;
+                        case "change_contact" :
+                            if (changeType.equals("create_user")){
+                                String userID = root.getElementsByTagName("UserID").item(0).getTextContent();
+                                QwUser qw = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, userID);
+                                QwUser qwUser = new QwUser();
+                                qwUser.setQwUserId(userID);
+                                qwUser.setCreateTime(new Date());
+                                qwUser.setCorpId(corpId);
+
+                                if (qw==null){
+                                    String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), userID,qwCompany.getPermanentCode());
+                                    qwUser.setQwUserId(userID);
+                                    qwUser.setDepartment(root.getElementsByTagName("Department").item(0).getTextContent().toString());
+                                    qwUser.setQwUserName(serverQwUserName);
+                                    QwOpenidByUserParams param1=new QwOpenidByUserParams();
+                                    param1.setUserid(userID);
+                                    QwOpenidResult qwOpenid = qwApiService.useridToOpenid(param1, corpId);
+                                    qwUser.setOpenid(qwOpenid.getOpenid());
+                                    qwUserMapper.insertQwUser(qwUser);
+                                }else {
+                                    qw.setIsDel(0);
+                                    qw.setUpdateTime(new Date());
+                                    qwUserMapper.updateQwUser(qw);
+                                }
+                            }
+                            else if (changeType.equals("delete_user")){
+                                String userID = root.getElementsByTagName("UserID").item(0).getTextContent();
+                                QwUser qwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, userID);
+                                if (qwUser!=null){
+                                    qwUser.setIsDel(2);
+                                    qwUser.setStatus(0);
+                                    qwUserMapper.updateQwUser(qwUser);
+                                    QwExternalContact qwExternalContact = new QwExternalContact();
+                                    qwExternalContact.setUserId(userID);
+                                    qwExternalContact.setStatus(0);
+                                    qwExternalContact.setCorpId(corpId);
+                                    List<QwExternalContact> qwExternalContacts = qwExternalContactService.selectQwExternalContactList(qwExternalContact);
+                                    for (QwExternalContact externalContact : qwExternalContacts) {
+                                        QwExternalContact lz = new QwExternalContact();
+                                        lz.setId(externalContact.getId());
+                                        lz.setStatus(1);
+                                        lz.setCorpId(corpId);
+                                        qwExternalContactMapper.updateQwExternalContact(lz);
+                                    }
+                                }
+                            }
+                            else if (changeType.equals("create_party")){
 
-                        qwDeptService.insertOrUpdateQwDept(corpId);
+                                qwDeptService.insertOrUpdateQwDept(corpId);
+                            }
+                            else if (changeType.equals("update_party")){
 
-                    }else if (changeType.equals("delete_party")){
-                        QwDept qwDept=new QwDept();
-                        qwDept.setDeptId(Long.valueOf(root.getElementsByTagName("Id").item(0).getTextContent()));
-                        qwDeptService.deleteQwDeptByDeptId(qwDept);
-                    }
+                                qwDeptService.insertOrUpdateQwDept(corpId);
 
-                    break;
-                case "change_external_contact" :
-                    switch (changeType){
-                        case "add_external_contact":
-                            String State =null;
-                            NodeList StateList = root.getElementsByTagName("State");
-                            if(StateList.getLength() > 0) {
-                                State = StateList.item(0).getTextContent();
-                            }
-                            String WelcomeCode =null;
-                            NodeList WelcomeCodeList = root.getElementsByTagName("WelcomeCode");
-                            if(WelcomeCodeList.getLength() > 0) {
-                                WelcomeCode = WelcomeCodeList.item(0).getTextContent();
+                            }else if (changeType.equals("delete_party")){
+                                QwDept qwDept=new QwDept();
+                                qwDept.setDeptId(Long.valueOf(root.getElementsByTagName("Id").item(0).getTextContent()));
+                                qwDeptService.deleteQwDeptByDeptId(qwDept);
                             }
 
-                            String userId = root.getElementsByTagName("UserID").item(0).getTextContent();
-                            String externalUserId = root.getElementsByTagName("ExternalUserID").item(0).getTextContent();
-                            String cacheKey = "qwApiExternal:" + userId + ":" + corpId + ":" + externalUserId;
-                            String lockKey = "lock:qwApiExternal:" + userId + ":" + corpId + ":" + externalUserId; // 锁Key(Hash类型,加前缀lock:)
-
-                            // 2. 获取 Redisson 分布式锁
-                            RLock lock = redissonClient.getLock(lockKey);
-                            boolean isLocked = false;
-                            try {
-                                // 3. 尝试加锁:最多等待 5 秒,锁自动释放时间 15 分钟
-                                isLocked = lock.tryLock(5, 15, TimeUnit.MINUTES);
-                                if (isLocked) {
-                                    // 4. 加锁成功后,再次检查缓存(避免多线程竞争时重复执行业务)
-                                    String qwApiExternal = redisCache.getCacheObject(cacheKey);
-                                    if (StringUtil.strIsNullOrEmpty(qwApiExternal)) {
-                                        try {
-                                            // 5. 新增用户
-                                            qwExternalContactService.insertQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),null,corpId,State,WelcomeCode);
-                                            // 6. 业务逻辑执行成功后,写入 Redis 缓存(有效期 10 分钟)
-                                            redisCache.setCacheObject(cacheKey, "1", 10, TimeUnit.MINUTES);
-
-                                            // 广告线索处理
-                                            leadService.updateAddMemberLead(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId,State);
-                                        } catch (Exception e) {
-                                            // 7. 业务逻辑失败时,删除缓存
-                                            redisCache.deleteObject(cacheKey);
+                            break;
+                        case "change_external_contact" :
+                            switch (changeType){
+                                case "add_external_contact":
+                                    String State =null;
+                                    NodeList StateList = root.getElementsByTagName("State");
+                                    if(StateList.getLength() > 0) {
+                                        State = StateList.item(0).getTextContent();
+                                    }
+                                    String WelcomeCode =null;
+                                    NodeList WelcomeCodeList = root.getElementsByTagName("WelcomeCode");
+                                    if(WelcomeCodeList.getLength() > 0) {
+                                        WelcomeCode = WelcomeCodeList.item(0).getTextContent();
+                                    }
+
+                                    String userId = root.getElementsByTagName("UserID").item(0).getTextContent();
+                                    String externalUserId = root.getElementsByTagName("ExternalUserID").item(0).getTextContent();
+                                    String cacheKey = "qwApiExternal:" + userId + ":" + corpId + ":" + externalUserId;
+                                    String lockKey = "lock:qwApiExternal:" + userId + ":" + corpId + ":" + externalUserId; // 锁Key(Hash类型,加前缀lock:)
+
+                                    // 2. 获取 Redisson 分布式锁
+                                    RLock lock = redissonClient.getLock(lockKey);
+                                    boolean isLocked = false;
+                                    try {
+                                        // 3. 尝试加锁:最多等待 5 秒,锁自动释放时间 15 分钟
+                                        isLocked = lock.tryLock(5, 15, TimeUnit.MINUTES);
+                                        if (isLocked) {
+                                            // 4. 加锁成功后,再次检查缓存(避免多线程竞争时重复执行业务)
+                                            String qwApiExternal = redisCache.getCacheObject(cacheKey);
+                                            if (StringUtil.strIsNullOrEmpty(qwApiExternal)) {
+                                                try {
+                                                    // 5. 新增用户
+                                                    qwExternalContactService.insertQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),null,corpId,State,WelcomeCode);
+                                                    // 6. 业务逻辑执行成功后,写入 Redis 缓存(有效期 10 分钟)
+                                                    redisCache.setCacheObject(cacheKey, "1", 10, TimeUnit.MINUTES);
+
+                                                    // 广告线索处理
+                                                    leadService.updateAddMemberLead(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId,State);
+                                                } catch (Exception e) {
+                                                    // 7. 业务逻辑失败时,删除缓存
+                                                    redisCache.deleteObject(cacheKey);
+                                                }
+                                            }
+                                        }
+                                    } catch (InterruptedException e) {
+                                        logger.error("中断异常");
+                                    } finally {
+                                        // 4. 确保锁最终被释放(只有加锁成功的线程才需要释放)
+                                        if (isLocked && lock.isHeldByCurrentThread()) {
+                                            lock.unlock();
                                         }
                                     }
-                                }
-                            } catch (InterruptedException e) {
-                                logger.error("中断异常");
-                            } finally {
-                                // 4. 确保锁最终被释放(只有加锁成功的线程才需要释放)
-                                if (isLocked && lock.isHeldByCurrentThread()) {
-                                    lock.unlock();
-                                }
-                            }
 
+                                    break;
+                                case "edit_external_contact":
+                                    qwExternalContactService.updateQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
+                                    break;
+                                case "del_external_contact":
+                                    qwExternalContactService.deleteQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
+                                    break;
+                                case "del_follow_user":
+                                    qwExternalContactService.deletefollowUserByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
+                                    // 广告线索处理
+                                    leadService.updateDeleteMemberLead(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
+                                    break;
+                                case "transfer_fail":
+                                    qwExternalContactService.transferFailByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId,root.getElementsByTagName("FailReason").item(0).getTextContent());
+                                    break;
+                            }
                             break;
-                        case "edit_external_contact":
-                            qwExternalContactService.updateQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
-                            break;
-                        case "del_external_contact":
-                            qwExternalContactService.deleteQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
-                            break;
-                        case "del_follow_user":
-                            qwExternalContactService.deletefollowUserByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
-                            // 广告线索处理
-                            leadService.updateDeleteMemberLead(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);
-                            break;
-                        case "transfer_fail":
-                            qwExternalContactService.transferFailByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId,root.getElementsByTagName("FailReason").item(0).getTextContent());
-                            break;
-                    }
-                    break;
-                case "change_external_tag":
+                        case "change_external_tag":
+
+                            switch (changeType){
+                                case "create":
+                                    qwTagGroupService.addTagByTagId(root.getElementsByTagName("TagType").item(0).getTextContent(),root.getElementsByTagName("Id").item(0).getTextContent(),corpId);
+                                    break;
+                                case "update":
+                                    qwTagGroupService.updateTagByTagId(root.getElementsByTagName("TagType").item(0).getTextContent(),root.getElementsByTagName("Id").item(0).getTextContent(),corpId);
+                                    break;
+                                case "delete":
+                                    qwTagGroupService.deleteTagByTagId(root.getElementsByTagName("TagType").item(0).getTextContent(),root.getElementsByTagName("Id").item(0).getTextContent(),corpId);
+                                    break;
+                                case "shuffle":
+                                    qwTagGroupService.shuffleTagByTagId(root.getElementsByTagName("Id").item(0).getTextContent(),corpId);
+                                    break;
+
+                            }
 
-                    switch (changeType){
-                        case "create":
-                            qwTagGroupService.addTagByTagId(root.getElementsByTagName("TagType").item(0).getTextContent(),root.getElementsByTagName("Id").item(0).getTextContent(),corpId);
-                            break;
-                        case "update":
-                            qwTagGroupService.updateTagByTagId(root.getElementsByTagName("TagType").item(0).getTextContent(),root.getElementsByTagName("Id").item(0).getTextContent(),corpId);
-                            break;
-                        case "delete":
-                            qwTagGroupService.deleteTagByTagId(root.getElementsByTagName("TagType").item(0).getTextContent(),root.getElementsByTagName("Id").item(0).getTextContent(),corpId);
-                            break;
-                        case "shuffle":
-                            qwTagGroupService.shuffleTagByTagId(root.getElementsByTagName("Id").item(0).getTextContent(),corpId);
                             break;
+                        case "change_external_chat":
 
-                    }
+                            switch (changeType){
+                                case "create":
+                                    //新建的客户群
+                                    qwGroupChatService.insertQwGroupChat(root.getElementsByTagName("ChatId").item(0).getTextContent(),corpId);
+                                    break;
+                                case "update":
 
-                    break;
-                case "change_external_chat":
+                                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
-                    switch (changeType){
-                        case "create":
-                            //新建的客户群
-                            qwGroupChatService.insertQwGroupChat(root.getElementsByTagName("ChatId").item(0).getTextContent(),corpId);
-                            break;
-                        case "update":
-
-                            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
-                            //群id
-                            String chatId = root.getElementsByTagName("ChatId").item(0).getTextContent();
-                            //变更详情
-                            String updateDetail = root.getElementsByTagName("UpdateDetail").item(0).getTextContent();
-
-                            //群信息
-                            QwGroupChat qwGroupChat=new QwGroupChat();
-
-                            //成员详细信息
-                            QwGroupChatUser qwGroupChatUser=new QwGroupChatUser();
-                            qwGroupChatUser.setCorpId(corpId);
-                            //成员入群
-                            if (updateDetail.equals("add_member")){
-                                QwGroupChatDetailsResult chat = qwApiService.groupChatDetails(chatId, corpId);
-                                if(chat.getErrCode() != 0){
-                                    log.error("获取群聊信息失败: {}", chat.getErrMsg());
-                                    return;
-                                }
-                                List<QwGroupChatDetailsResult.Member> memberList = chat.getGroupChat().getMemberList();
-                                Map<String, QwGroupChatDetailsResult.Member> memberMap = PubFun.listToMapByGroupObject(memberList, QwGroupChatDetailsResult.Member::getUserId);
+                                    //群id
+                                    String chatId = root.getElementsByTagName("ChatId").item(0).getTextContent();
+                                    //变更详情
+                                    String updateDetail = root.getElementsByTagName("UpdateDetail").item(0).getTextContent();
+
+                                    //群信息
+                                    QwGroupChat qwGroupChat=new QwGroupChat();
+
+                                    //成员详细信息
+                                    QwGroupChatUser qwGroupChatUser=new QwGroupChatUser();
+                                    qwGroupChatUser.setCorpId(corpId);
+                                    //成员入群
+                                    if (updateDetail.equals("add_member")){
+                                        QwGroupChatDetailsResult chat = qwApiService.groupChatDetails(chatId, corpId);
+                                        if(chat.getErrCode() != 0){
+                                            log.error("获取群聊信息失败: {}", chat.getErrMsg());
+                                            return;
+                                        }
+                                        List<QwGroupChatDetailsResult.Member> memberList = chat.getGroupChat().getMemberList();
+                                        Map<String, QwGroupChatDetailsResult.Member> memberMap = PubFun.listToMapByGroupObject(memberList, QwGroupChatDetailsResult.Member::getUserId);
 
-                                //入群方式
-                                String joinScene = root.getElementsByTagName("JoinScene").item(0).getTextContent();
+                                        //入群方式
+                                        String joinScene = root.getElementsByTagName("JoinScene").item(0).getTextContent();
 
-                                //消息创建时间
+                                        //消息创建时间
 //                                    String CreateTime = root.getElementsByTagName("CreateTime").item(0).getTextContent();
 //                                    Date date1 = new Date(Long.valueOf(CreateTime) * 1000); // 乘以1000是因为时间戳是以秒为单位,而Date期望毫秒
 //                                    String formattedDate = sdf.format(date1);
-                                String formattedDate = sdf.format(new Date());
+                                        String formattedDate = sdf.format(new Date());
 
-                                //入群数量
-                                String MemChangeCnt = root.getElementsByTagName("MemChangeCnt").item(0).getTextContent();
+                                        //入群数量
+                                        String MemChangeCnt = root.getElementsByTagName("MemChangeCnt").item(0).getTextContent();
 
-                                // 获取MemChangeList中的Item元素,当是成员入群或退群时有值。变更的成员列表
-                                NodeList items = document.getElementsByTagName("Item");
+                                        // 获取MemChangeList中的Item元素,当是成员入群或退群时有值。变更的成员列表
+                                        NodeList items = document.getElementsByTagName("Item");
 
-                                QwAutoTags qwAutoTags = qwAutoTagsService.selectQwAutoTagsByChatIdJSON(corpId, chatId);
-                                logger.info("成员入群:qwAutoTags:"+qwAutoTags);
-                                //存分时段里符合条件的标签
-                                Set<String> combinedTagsItem = new HashSet<>();
+                                        QwAutoTags qwAutoTags = qwAutoTagsService.selectQwAutoTagsByChatIdJSON(corpId, chatId);
+                                        logger.info("成员入群:qwAutoTags:"+qwAutoTags);
+                                        //存分时段里符合条件的标签
+                                        Set<String> combinedTagsItem = new HashSet<>();
 
-                                //添加标签的日志记录
-                                QwAutoTagsLogs qwAutoTagsLogs=new QwAutoTagsLogs();
+                                        //添加标签的日志记录
+                                        QwAutoTagsLogs qwAutoTagsLogs=new QwAutoTagsLogs();
 
-                                //添加的标签
-                                QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
+                                        //添加的标签
+                                        QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
 
-                                boolean isMatch = false;
+                                        boolean isMatch = false;
 
-                                //查出 符合的标签
-                                if (qwAutoTags != null) {
-                                    List<QwAutoRulesTagsParams> qwAutoRulesTagsParams = JSON.parseArray(qwAutoTags.getRulesTags(), QwAutoRulesTagsParams.class);
-                                    for (QwAutoRulesTagsParams qrtp : qwAutoRulesTagsParams) {
-                                        if (qrtp.getRules().contains(chatId));{
-                                            combinedTagsItem.addAll(qrtp.getTags());
-                                            isMatch=true;
-                                            break;
-                                        }
+                                        //查出 符合的标签
+                                        if (qwAutoTags != null) {
+                                            List<QwAutoRulesTagsParams> qwAutoRulesTagsParams = JSON.parseArray(qwAutoTags.getRulesTags(), QwAutoRulesTagsParams.class);
+                                            for (QwAutoRulesTagsParams qrtp : qwAutoRulesTagsParams) {
+                                                if (qrtp.getRules().contains(chatId));{
+                                                    combinedTagsItem.addAll(qrtp.getTags());
+                                                    isMatch=true;
+                                                    break;
+                                                }
 
-                                    }
+                                            }
 
-                                }
+                                        }
 
-                                logger.info("成员入群items:"+items.getLength());
-                                //成员列表
-                                for (int i = 0; i < items.getLength(); i++) {
-
-                                    String userid = items.item(i).getTextContent();
-                                    QwGroupChatDetailsResult.Member member = memberMap.get(userid);
-                                    qwGroupChatUser.setChatId(chatId);
-                                    qwGroupChatUser.setUserId(userid);
-                                    qwGroupChatUser.setType(member.getType() + "");
-                                    qwGroupChatUser.setName(member.getName());
-                                    qwGroupChatUser.setUnionid(member.getUnionid());
-                                    qwGroupChatUser.setJoinScene(joinScene);
-                                    qwGroupChatUser.setJoinTime(formattedDate);
-
-                                    //这个群有符合条件的标签
-                                    if (isMatch){
-
-                                        //根据客户id获取 该客户属于哪个成员
-                                        List<QwExternalContact> qwExternalContacts = qwExternalContactService.selectQwExternalContactByExternalUserId(userid, corpId);
-                                        qwExternalContacts.stream().forEach(item->{
-
-                                            qwEditUserTagParam.setUserid(item.getUserId());
-                                            qwEditUserTagParam.setExternal_userid(userid);
-                                            //总标签 转换回列表
-                                            List<String> combinedTagsList = new ArrayList<>(combinedTagsItem);
-                                            // 设置标签
-                                            qwEditUserTagParam.setAdd_tag(combinedTagsList);
-
-
-                                            qwAutoTagsLogs.setAutoTagId(qwAutoTags.getId());
-                                            qwAutoTagsLogs.setType(2L);
-                                            qwAutoTagsLogs.setQwUserid(item.getQwUserId());
-                                            qwAutoTagsLogs.setExternalUserId(userid);
-                                            qwAutoTagsLogs.setEffectiveRules(JSON.toJSONString(combinedTagsList));
-                                            qwAutoTagsLogs.setAddTime(new Date());
-                                            qwAutoTagsLogs.setChatId(chatId);
-                                            qwAutoTagsLogs.setJoinScene(joinScene);
-                                            qwAutoTagsLogs.setCorpId(corpId);
-                                            //  qwAutoTagsLogs.setCompanyId(companyId);
-                                            // 企微加标签
-                                            QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, corpId);
-                                            logger.info("自动添加标签-入群行为:符合条件 " + qwResult);
-                                            qwAutoTagsLogsService.insertQwAutoTagsLogs(qwAutoTagsLogs);
-                                        });
+                                        logger.info("成员入群items:"+items.getLength());
+                                        //成员列表
+                                        for (int i = 0; i < items.getLength(); i++) {
+
+                                            String userid = items.item(i).getTextContent();
+                                            QwGroupChatDetailsResult.Member member = memberMap.get(userid);
+                                            qwGroupChatUser.setChatId(chatId);
+                                            qwGroupChatUser.setUserId(userid);
+                                            qwGroupChatUser.setType(member.getType() + "");
+                                            qwGroupChatUser.setName(member.getName());
+                                            qwGroupChatUser.setUnionid(member.getUnionid());
+                                            qwGroupChatUser.setJoinScene(joinScene);
+                                            qwGroupChatUser.setJoinTime(formattedDate);
+
+                                            //这个群有符合条件的标签
+                                            if (isMatch){
+
+                                                //根据客户id获取 该客户属于哪个成员
+                                                List<QwExternalContact> qwExternalContacts = qwExternalContactService.selectQwExternalContactByExternalUserId(userid, corpId);
+                                                qwExternalContacts.stream().forEach(item->{
+
+                                                    qwEditUserTagParam.setUserid(item.getUserId());
+                                                    qwEditUserTagParam.setExternal_userid(userid);
+                                                    //总标签 转换回列表
+                                                    List<String> combinedTagsList = new ArrayList<>(combinedTagsItem);
+                                                    // 设置标签
+                                                    qwEditUserTagParam.setAdd_tag(combinedTagsList);
+
+
+                                                    qwAutoTagsLogs.setAutoTagId(qwAutoTags.getId());
+                                                    qwAutoTagsLogs.setType(2L);
+                                                    qwAutoTagsLogs.setQwUserid(item.getQwUserId());
+                                                    qwAutoTagsLogs.setExternalUserId(userid);
+                                                    qwAutoTagsLogs.setEffectiveRules(JSON.toJSONString(combinedTagsList));
+                                                    qwAutoTagsLogs.setAddTime(new Date());
+                                                    qwAutoTagsLogs.setChatId(chatId);
+                                                    qwAutoTagsLogs.setJoinScene(joinScene);
+                                                    qwAutoTagsLogs.setCorpId(corpId);
+                                                    //  qwAutoTagsLogs.setCompanyId(companyId);
+                                                    // 企微加标签
+                                                    QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, corpId);
+                                                    logger.info("自动添加标签-入群行为:符合条件 " + qwResult);
+                                                    qwAutoTagsLogsService.insertQwAutoTagsLogs(qwAutoTagsLogs);
+                                                });
+
+                                            }
+
+
+                                            //查一遍有没有这个人-有的话 更新一下(退群后再入群的人)
+                                            QwGroupChatUser qwGroupChatUserOld = qwGroupChatUserService.selectQwGroupChatUserByExternalUserId(qwGroupChatUser);
+
+                                            if (qwGroupChatUserOld==null) {
+                                                qwGroupChatUserService.insertQwGroupChatUser(qwGroupChatUser);
+                                                // 群员入群 广告判断记录
+                                                leadService.updateGroupAddMemberLead(qwGroupChatUser.getName(),qwGroupChatUser.getChatId(),qwGroupChatUser.getCorpId(),qwGroupChatUser.getUnionid());
+                                            }else {
+                                                qwGroupChatUserOld.setIsOut(1L);
+                                                qwGroupChatUserOld.setCorpId(corpId);
+                                                qwGroupChatUserOld.setJoinScene(joinScene);
+                                                qwGroupChatUserOld.setJoinTime(formattedDate);
+                                                qwGroupChatUserOld.setQuitScene(null);
+                                                qwGroupChatUserOld.setOutTime(null);
+                                                qwGroupChatUserService.groupChatUserJoinAgain(qwGroupChatUserOld);
+                                            }
 
+                                        }
+                                        //先查一遍(后期可直接改sql,不查)
+                                        QwGroupChat qwGroupChatOld = qwGroupChatService.selectQwGroupChatByChatId(chatId);
+                                        if (qwGroupChatOld != null) {
+                                            qwGroupChatOld.setCorpId(corpId);
+                                            //当日入群数
+                                            qwGroupChatOld.setTodayJoin(qwGroupChatOld.getTodayJoin()+Integer.valueOf(MemChangeCnt));
+                                            //当前群人总数
+                                            qwGroupChatOld.setGroupSize(qwGroupChatOld.getGroupSize()+Integer.valueOf(MemChangeCnt));
+
+                                            qwGroupChatService.updateQwGroupChat(qwGroupChatOld,corpId);
+                                        }
                                     }
+                                    //成员退群
+                                    if (updateDetail.equals("del_member")){
+
+                                        //退群数量
+                                        String MemChangeCnt = root.getElementsByTagName("MemChangeCnt").item(0).getTextContent();
+                                        //退群方式
+                                        String QuitScene = root.getElementsByTagName("QuitScene").item(0).getTextContent();
+
+                                        String CreateTime = root.getElementsByTagName("CreateTime").item(0).getTextContent();
+                                        Date date1 = new Date(Long.valueOf(CreateTime) * 1000); // 乘以1000是因为时间戳是以秒为单位,而Date期望毫秒
+                                        String formattedDate = sdf.format(date1);
+
+                                        // 获取MemChangeList中的Item元素,当是成员入群或退群时有值。变更的成员列表
+                                        NodeList items = document.getElementsByTagName("Item");
+
+                                        //成员列表
+                                        for (int i = 0; i < items.getLength(); i++) {
+                                            String userid = items.item(i).getTextContent();
+                                            //是否退群
+                                            qwGroupChatUser.setIsOut(2L);
+                                            //退群时间
+                                            qwGroupChatUser.setOutTime(formattedDate);
+
+                                            qwGroupChatUser.setChatId(chatId);
+                                            qwGroupChatUser.setUserId(userid);
+                                            //退群方式
+                                            qwGroupChatUser.setQuitScene(Integer.parseInt(QuitScene));
+                                            qwGroupChatUser.setCorpId(corpId);
+                                            //更新
+                                            qwGroupChatUserService.upDateDelGroupChatUser(qwGroupChatUser);
+                                        }
 
+                                        //先查一遍(后期可直接改sql,不查)
+                                        QwGroupChat qwGroupChatOld = qwGroupChatService.selectQwGroupChatByChatId(chatId);
+                                        if (qwGroupChatOld != null) {
+                                            int memChangeCount;
 
-                                    //查一遍有没有这个人-有的话 更新一下(退群后再入群的人)
-                                    QwGroupChatUser qwGroupChatUserOld = qwGroupChatUserService.selectQwGroupChatUserByExternalUserId(qwGroupChatUser);
-
-                                    if (qwGroupChatUserOld==null) {
-                                        qwGroupChatUserService.insertQwGroupChatUser(qwGroupChatUser);
-                                        // 群员入群 广告判断记录
-                                        leadService.updateGroupAddMemberLead(qwGroupChatUser.getName(),qwGroupChatUser.getChatId(),qwGroupChatUser.getCorpId(),qwGroupChatUser.getUnionid());
-                                    }else {
-                                        qwGroupChatUserOld.setIsOut(1L);
-                                        qwGroupChatUserOld.setCorpId(corpId);
-                                        qwGroupChatUserOld.setJoinScene(joinScene);
-                                        qwGroupChatUserOld.setJoinTime(formattedDate);
-                                        qwGroupChatUserOld.setQuitScene(null);
-                                        qwGroupChatUserOld.setOutTime(null);
-                                        qwGroupChatUserService.groupChatUserJoinAgain(qwGroupChatUserOld);
-                                    }
+                                            if (MemChangeCnt == null || MemChangeCnt.isEmpty()) {
+                                                memChangeCount = 1;
+                                            } else {
+                                                try {
+                                                    memChangeCount = Integer.parseInt(MemChangeCnt);
+                                                } catch (NumberFormatException e) {
+                                                    memChangeCount = 1;
+                                                }
+                                            }
 
-                                }
-                                //先查一遍(后期可直接改sql,不查)
-                                QwGroupChat qwGroupChatOld = qwGroupChatService.selectQwGroupChatByChatId(chatId);
-                                if (qwGroupChatOld != null) {
-                                    qwGroupChatOld.setCorpId(corpId);
-                                    //当日入群数
-                                    qwGroupChatOld.setTodayJoin(qwGroupChatOld.getTodayJoin()+Integer.valueOf(MemChangeCnt));
-                                    //当前群人总数
-                                    qwGroupChatOld.setGroupSize(qwGroupChatOld.getGroupSize()+Integer.valueOf(MemChangeCnt));
-
-                                    qwGroupChatService.updateQwGroupChat(qwGroupChatOld,corpId);
-                                }
-                            }
-                            //成员退群
-                            if (updateDetail.equals("del_member")){
-
-                                //退群数量
-                                String MemChangeCnt = root.getElementsByTagName("MemChangeCnt").item(0).getTextContent();
-                                //退群方式
-                                String QuitScene = root.getElementsByTagName("QuitScene").item(0).getTextContent();
-
-                                String CreateTime = root.getElementsByTagName("CreateTime").item(0).getTextContent();
-                                Date date1 = new Date(Long.valueOf(CreateTime) * 1000); // 乘以1000是因为时间戳是以秒为单位,而Date期望毫秒
-                                String formattedDate = sdf.format(date1);
-
-                                // 获取MemChangeList中的Item元素,当是成员入群或退群时有值。变更的成员列表
-                                NodeList items = document.getElementsByTagName("Item");
-
-                                //成员列表
-                                for (int i = 0; i < items.getLength(); i++) {
-                                    String userid = items.item(i).getTextContent();
-                                    //是否退群
-                                    qwGroupChatUser.setIsOut(2L);
-                                    //退群时间
-                                    qwGroupChatUser.setOutTime(formattedDate);
-
-                                    qwGroupChatUser.setChatId(chatId);
-                                    qwGroupChatUser.setUserId(userid);
-                                    //退群方式
-                                    qwGroupChatUser.setQuitScene(Integer.parseInt(QuitScene));
-                                    qwGroupChatUser.setCorpId(corpId);
-                                    //更新
-                                    qwGroupChatUserService.upDateDelGroupChatUser(qwGroupChatUser);
-                                }
-
-                                //先查一遍(后期可直接改sql,不查)
-                                QwGroupChat qwGroupChatOld = qwGroupChatService.selectQwGroupChatByChatId(chatId);
-                                if (qwGroupChatOld != null) {
-                                    int memChangeCount;
-
-                                    if (MemChangeCnt == null || MemChangeCnt.isEmpty()) {
-                                        memChangeCount = 1;
-                                    } else {
-                                        try {
-                                            memChangeCount = Integer.parseInt(MemChangeCnt);
-                                        } catch (NumberFormatException e) {
-                                            memChangeCount = 1;
-                                        }
-                                    }
+                                            // 确保qwGroupChatOld不是null
+                                            if (qwGroupChatOld != null) {
+                                                //当日退群数
+                                                Long todayOut = qwGroupChatOld.getTodayOut();
+                                                if (todayOut != null) {
 
-                                    // 确保qwGroupChatOld不是null
-                                    if (qwGroupChatOld != null) {
-                                        //当日退群数
-                                        Long todayOut = qwGroupChatOld.getTodayOut();
-                                        if (todayOut != null) {
+                                                    qwGroupChatOld.setTodayJoin(todayOut + (long) memChangeCount);
+                                                } else {
 
-                                            qwGroupChatOld.setTodayJoin(todayOut + (long) memChangeCount);
-                                        } else {
+                                                    qwGroupChatOld.setTodayJoin((long) memChangeCount); // 或者根据实际情况决定如何处理
+                                                }
 
-                                            qwGroupChatOld.setTodayJoin((long) memChangeCount); // 或者根据实际情况决定如何处理
-                                        }
+                                                //总退群数
+                                                qwGroupChatOld.setAllOutGroup(qwGroupChatOld.getAllOutGroup()+memChangeCount);
 
-                                        //总退群数
-                                        qwGroupChatOld.setAllOutGroup(qwGroupChatOld.getAllOutGroup()+memChangeCount);
+                                                long newGroupSize = (qwGroupChatOld.getGroupSize() - memChangeCount) < 0 ? 0 : qwGroupChatOld.getGroupSize() - memChangeCount;
 
-                                        long newGroupSize = (qwGroupChatOld.getGroupSize() - memChangeCount) < 0 ? 0 : qwGroupChatOld.getGroupSize() - memChangeCount;
+                                                //当前群人总数
+                                                qwGroupChatOld.setGroupSize(newGroupSize);
+                                                qwGroupChatOld.setCorpId(corpId);
 
-                                        //当前群人总数
-                                        qwGroupChatOld.setGroupSize(newGroupSize);
-                                        qwGroupChatOld.setCorpId(corpId);
+                                                qwGroupChatService.updateQwGroupChat(qwGroupChatOld,corpId);
 
-                                        qwGroupChatService.updateQwGroupChat(qwGroupChatOld,corpId);
+                                            }
+                                        }
 
                                     }
-                                }
+                                    //群主变更/群名变更/群公告变更
+                                    if (updateDetail.equals("change_owner")||updateDetail.equals("change_name")||updateDetail.equals("change_notice")){
 
-                            }
-                            //群主变更/群名变更/群公告变更
-                            if (updateDetail.equals("change_owner")||updateDetail.equals("change_name")||updateDetail.equals("change_notice")){
 
+                                        //获取群聊详情
+                                        QwGroupChatDetailsResult qwGroupChatDetailsResult = qwApiService.groupChatDetails(chatId, corpId);
 
-                                //获取群聊详情
-                                QwGroupChatDetailsResult qwGroupChatDetailsResult = qwApiService.groupChatDetails(chatId, corpId);
+                                        if (qwGroupChatDetailsResult.getErrCode()==0){
+                                            QwGroupChatDetailsResult.GroupChats groupChat = qwGroupChatDetailsResult.getGroupChat();
 
-                                if (qwGroupChatDetailsResult.getErrCode()==0){
-                                    QwGroupChatDetailsResult.GroupChats groupChat = qwGroupChatDetailsResult.getGroupChat();
+                                            //群名
+                                            String name = groupChat.getName();
+                                            //群公告
+                                            String notice = groupChat.getNotice();
+                                            //群主id
+                                            String owner = groupChat.getOwner();
 
-                                    //群名
-                                    String name = groupChat.getName();
-                                    //群公告
-                                    String notice = groupChat.getNotice();
-                                    //群主id
-                                    String owner = groupChat.getOwner();
+                                            qwGroupChat.setName(name);
+                                            qwGroupChat.setNotice(notice);
+                                            qwGroupChat.setOwner(owner);
+                                            qwGroupChat.setMemberVersion(groupChat.getMemberVersion());
+                                            qwGroupChat.setCorpId(corpId);
+                                            qwGroupChat.setChatId(chatId);
+                                            qwGroupChat.setUpdateTime(DateUtils.getNowDate());
+                                            //更新
+                                            qwGroupChatService.updateQwGroupChat(qwGroupChat,corpId);
 
-                                    qwGroupChat.setName(name);
-                                    qwGroupChat.setNotice(notice);
-                                    qwGroupChat.setOwner(owner);
-                                    qwGroupChat.setMemberVersion(groupChat.getMemberVersion());
-                                    qwGroupChat.setCorpId(corpId);
-                                    qwGroupChat.setChatId(chatId);
-                                    qwGroupChat.setUpdateTime(DateUtils.getNowDate());
-                                    //更新
-                                    qwGroupChatService.updateQwGroupChat(qwGroupChat,corpId);
+                                        }
+                                    }
 
-                                }
+                                    break;
+                                case "dismiss":
+                                    //解散客户群(删掉!)
+                                    qwGroupChatService.deleteQwGroupChatByChatIdAndCompanyId(root.getElementsByTagName("ChatId").item(0).getTextContent(),corpId);
+                                    break;
                             }
 
                             break;
-                        case "dismiss":
-                            //解散客户群(删掉!)
-                            qwGroupChatService.deleteQwGroupChatByChatIdAndCompanyId(root.getElementsByTagName("ChatId").item(0).getTextContent(),corpId);
-                            break;
+                        default:
+                            logger.info("默认:"+event);
                     }
-
-                    break;
-                default:
-                    logger.info("默认:"+event);
-            }
+                }
+        } catch (Exception e) {
+            logger.error("[QwCallback] 解密回调数据失败,corpId={}, tenantId={}",
+                    corpId, qwCompany.getTenantId(), e);
+        } finally {
+            // 清理租户上下文
+            RedisTenantContext.clear();
+            tenantDataSourceManager.clear();
         }
     }
 

+ 31 - 0
fs-qw-api/src/main/java/com/fs/app/service/impl/OpenQwApiServiceImpl.java

@@ -3,11 +3,13 @@ package com.fs.app.service.impl;
 import com.alibaba.fastjson.JSON;
 import com.fs.app.service.OpenQwApiService;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwDept;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwDeptMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
@@ -37,6 +39,7 @@ import com.fs.sop.params.SopUserLogsParamByDate;
 import com.fs.sop.service.ISopUserLogsService;
 import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
 import org.slf4j.MDC;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -69,6 +72,10 @@ public class OpenQwApiServiceImpl implements OpenQwApiService {
 
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    private QwCompanyMapper qwCompanyMapper;
+    @Autowired
+    private RedisCache redisCache;
 
     @Override
     public R getSyncQwUser(String corpId) {
@@ -625,6 +632,30 @@ public class OpenQwApiServiceImpl implements OpenQwApiService {
         return R.ok("成功:" + suc.get() + ",失败:" + failList);
     }
 
+    @Override
+    public R getOpenExternalUserid(String externalUserid,String corpId,String qwUserId) {
+        //先查询redis
+        String key = "qwExternalUserid:trans:"+corpId +":" + qwUserId +":"+externalUserid;
+        String openExternalUserid = redisCache.getCacheObject(key);
+        if (openExternalUserid == null) {
+            //查询数据库
+            QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalByApplicationExternalUserId(externalUserid,corpId,qwUserId);
+            if (externalContact != null) {
+                openExternalUserid = externalContact.getExternalUserId();
+            } else {
+                QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
+                String token1 = qwApiService.getToken(corpId, qwCompany.getPermanentCode());
+                openExternalUserid = qwApiService.getOpenExternalUserid(token1, externalUserid, corpId);
+            }
+            if (StringUtils.isNotBlank(openExternalUserid)) {
+                redisCache.setCacheObject(key, openExternalUserid);
+            }
+            return R.ok().put("openExternalUserid",openExternalUserid);
+        } else {
+            return R.ok().put("openExternalUserid",openExternalUserid);
+        }
+    }
+
     @Autowired
     private QwSopMapper qwSopMapper;
 

+ 19 - 2
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.fastGpt.service.impl;
 
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
@@ -53,6 +54,7 @@ import com.fs.qw.domain.*;
 import com.fs.qw.mapper.*;
 import com.fs.qw.param.QwAutoTagsRulesTags;
 import com.fs.qw.service.*;
+import com.fs.qwApi.config.OpenQwConfig;
 import com.fs.qwApi.domain.QwResult;
 import com.fs.qwApi.param.QwEditUserTagParam;
 import com.fs.qwApi.param.QwSendMsgParam;
@@ -410,6 +412,7 @@ public class AiHookServiceImpl implements AiHookService {
         }
         log.info("数据:{}", qwUserId);
         QwUser user = qwUserMapper.selectQwUserById(qwUserId);
+        String corpId = user.getCorpId();
         //查询接收人
         if(user==null){
             log.error("查询接收人为空");
@@ -459,8 +462,22 @@ public class AiHookServiceImpl implements AiHookService {
             return R.ok();
         }
         com.fs.wxwork.dto.WxWorkVid2UserIdRespDTO dto = data.get(0);
-
-        QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(dto.getOpenid(), user.getCorpId(),user.getQwUserId());
+        QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(dto.getOpenid(), corpId,user.getQwUserId());
+        try {
+            if (qwExternalContacts == null){
+                String url = OpenQwConfig.baseApi + "/getOpenExternalUserid?externalUserid=" + dto.getOpenid() + "&corpId=" + corpId + "&qwUserId=" + user.getQwUserId() + "&tenantId=" + tenantId;
+                String result = HttpUtil.createPost(url)
+                        .execute()
+                        .body();
+                R r = com.alibaba.fastjson.JSONObject.parseObject(result, R.class);
+                String openExternalUserid = r.get("openExternalUserid").toString();
+                if(StringUtils.isNotBlank(openExternalUserid)){
+                    qwExternalContacts = qwExternalContactMapper.selectQwExternalByApplicationExternalUserId(openExternalUserid, corpId,user.getQwUserId());
+                }
+            }
+        } catch (Exception e) {
+            log.error("未查询到外部联系人id:{},报错{}",e,dto.getOpenid());
+        }
         if (qwExternalContacts==null){
             log.error("没有外部联系人" + "user:" + user);
             return R.ok();

+ 5 - 0
fs-service/src/main/java/com/fs/qw/domain/QwExternalContact.java

@@ -156,5 +156,10 @@ public class QwExternalContact extends BaseEntity
     // 广告链路唯一id
     private String traceId;
 
+    /**
+     * 代开发应用的 应用外部联系人id
+     */
+    private String applicationExternalUserId;
+
 
 }

+ 4 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -88,7 +88,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
     public int batchUpdateQwExternalByIsDaysNotStudy(List<QwExternalContact> qwExternalContact);
 
 
-    @Select("SELECT * FROM qw_external_contact WHERE external_user_id = #{externalUserId}  AND corp_id=#{corpId} and user_id=#{qwUserId} limit 1")
+    @Select("SELECT * FROM qw_external_contact WHERE (external_user_id = #{externalUserId} or application_external_user_id = #{externalUserId} )AND corp_id=#{corpId} and user_id=#{qwUserId} limit 1")
     public QwExternalContact selectQwExternalContactByExternalUserIdAndQwUserId(@Param("externalUserId") String externalUserId,@Param("corpId") String corpId,@Param("qwUserId") String qwUserId);
 
     @Select("<script>" +
@@ -648,4 +648,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
             "</foreach>" +
             "</script>")
     public int batchUpdateQwExternalContactMandatoryRegistration(@Param("map") List<QwMandatoryRegistrParam> batchList);
+
+    QwExternalContact selectQwExternalByApplicationExternalUserId(@Param("externalUserid")String externalUserid,
+                                                                  @Param("corpId")String corpId,@Param("qwUserId") String qwUserId);
 }

+ 3 - 2
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -1027,6 +1027,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
 
 
                     // 设置公共属性
+                    qwExternalContact.setQwOpenUserId(qwUser.getQwOpenUserid());
                     qwExternalContact.setCompanyUserId(qwUser.getCompanyUserId());
                     qwExternalContact.setQwUserId(qwUser.getId());
                     qwExternalContact.setName(contact.getName()); // 设置名称
@@ -1195,8 +1196,8 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
                         content="您好,您的服务已升级,后续将由我的同事接替我的工作,继续为您服务。";
                     }
                     qwTransferCustomerParam.setTransfer_success_msg(content);
-                    qwTransferCustomerParam.setHandover_userid(qwExternalContact.getUserId());
-                    qwTransferCustomerParam.setTakeover_userid(qwUser.getQwUserId());
+                    qwTransferCustomerParam.setHandover_userid(qwExternalContact.getQwOpenUserId());
+                    qwTransferCustomerParam.setTakeover_userid(qwUser.getQwOpenUserid());
                     qwTransferCustomerParam.setExternal_userid(Arrays.asList(qwExternalContact.getExternalUserId()));
                     QwTransferCustomerResult qwTransferCustomerResult = qwApiService.transferCustomer(qwTransferCustomerParam, param.getCorpId());
 //                    logger.info("返回:{}",qwTransferCustomerResult);

+ 1 - 1
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -1319,7 +1319,7 @@ public class QwUserServiceImpl implements IQwUserService
         WxWorkSetCallbackUrlDTO wxWorkSetCallbackUrlDTO = new WxWorkSetCallbackUrlDTO();
 
         System.out.println("回调地址"+"http://saasqwapimsg.ylrzcloud.com/msg/callback/"+serverId + "/"+loginParam.getTenantId());
-        wxWorkSetCallbackUrlDTO.setUrl("http://saasqwapimsg.ylrzcloud.com/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
+        wxWorkSetCallbackUrlDTO.setUrl("http://cn-hk-bgp-4.ofalias.net:55081/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
         wxWorkSetCallbackUrlDTO.setUuid(data.getUuid());
         wxWorkService.SetCallbackUrl(wxWorkSetCallbackUrlDTO,serverId);
 

+ 5 - 3
fs-service/src/main/java/com/fs/qwApi/service/impl/QwApiServiceImpl.java

@@ -1689,7 +1689,7 @@ public class QwApiServiceImpl implements QwApiService {
     @Override
     public QwExternalContactResult getExternalcontact(String userId,String corpId) {
 
-        String appSecret = getAppSecret(corpId);
+//        String appSecret = getAppSecret(corpId);
         String permanentCode = getPermanentCode(corpId);
 
         String token1 = getToken(corpId, permanentCode);
@@ -1697,7 +1697,8 @@ public class QwApiServiceImpl implements QwApiService {
         try {
             URIBuilder builder = new URIBuilder(QwApiConfig.externalcontactUrl);
             builder.setParameter("access_token", token1);
-            builder.setParameter("external_userid", getOpenExternalUserid(token1,userId,corpId));
+            builder.setParameter("external_userid", userId);
+//            builder.setParameter("external_userid", getOpenExternalUserid(token1,userId,corpId));
             URI uri = builder.build();
             HttpGet httpGet = new HttpGet(uri);
             HttpResponse response = httpClient.execute(httpGet);
@@ -1849,7 +1850,8 @@ public class QwApiServiceImpl implements QwApiService {
 
     @Override
     public QwTransferCustomerResult transferCustomer(QwTransferCustomerParam param,String corpId) {
-        String json = sendPost(QwApiConfig.transferCustomerUrl, param,corpId,null);
+        QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
+        String json = sendPost(QwApiConfig.transferCustomerUrl, param,corpId,qwCompany.getPermanentCode());
         QwTransferCustomerResult qwResult = JSON.parseObject(json, QwTransferCustomerResult.class);
         return qwResult;
     }

+ 3 - 1
fs-service/src/main/java/com/fs/statis/impl/FsStatisQwWatchServiceImpl.java

@@ -84,8 +84,10 @@ public class FsStatisQwWatchServiceImpl implements IFsStatisQwWatchService {
             }
         }
         fsStatisQwTempParamMapper.clear();
+        if (paramList != null && !paramList.isEmpty()) {
+            fsStatisQwTempParamMapper.insertList(paramList);
+        }
 
-        fsStatisQwTempParamMapper.insertList(paramList);
 
         fsStatisQwWatchMapper.generateData(paramByDate);
 

+ 12 - 0
fs-service/src/main/java/com/fs/tenant/dto/MenuDto.java

@@ -0,0 +1,12 @@
+package com.fs.tenant.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class MenuDto {
+    private String id; // 租户ID
+    private List<Long> selected;  // 已选中菜单ID
+    private List<Long> unSelected;// 未选中菜单ID
+}

+ 8 - 0
fs-service/src/main/java/com/fs/tenant/mapper/TenantInfoMapper.java

@@ -1,8 +1,10 @@
 package com.fs.tenant.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.core.domain.entity.SysMenu;
 import com.fs.tenant.domain.TenantInfo;
 import com.fs.tenant.vo.TenantInfoShowVo;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
 
@@ -62,6 +64,12 @@ public interface TenantInfoMapper extends BaseMapper<TenantInfo> {
     int deleteTenantInfoByIds(String[] ids);
 
     List<TenantInfoShowVo> tenantList(TenantInfo tenantInfo);
+
+    List<SysMenu> selectTenantMenu();
+
+    int updatePitchMenu(@Param("selected") List<Long> selected);
+
+    int updateUnPitchMenu(@Param("unSelected") List<Long> unSelected);
 }
 
 

+ 5 - 0
fs-service/src/main/java/com/fs/tenant/service/TenantInfoService.java

@@ -1,6 +1,7 @@
 package com.fs.tenant.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
 import com.fs.tenant.domain.TenantInfo;
 import com.fs.tenant.vo.TenantInfoShowVo;
 
@@ -61,4 +62,8 @@ public interface TenantInfoService extends IService<TenantInfo> {
     int deleteTenantInfoById(String id);
 
     List<TenantInfoShowVo> tenantList(TenantInfo tenantInfo);
+
+    R menuChange(String id);
+
+    R menuEdit(List<Long> selected, List<Long> unSelected);
 }

+ 20 - 0
fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java

@@ -3,10 +3,12 @@ package com.fs.tenant.service.impl;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.TreeSelect;
 import com.fs.common.core.domain.entity.SysMenu;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
+import com.fs.system.service.ISysMenuService;
 import com.fs.tenant.domain.TenantInfo;
 import com.fs.tenant.mapper.TenantInfoMapper;
 import com.fs.tenant.service.TenantAsyncService;
@@ -30,6 +32,8 @@ import java.util.List;
 public class TenantInfoServiceImpl extends ServiceImpl<TenantInfoMapper, TenantInfo> implements TenantInfoService {
     @Autowired
     private TenantAsyncService tenantAsyncService;
+    @Autowired
+    private ISysMenuService menuService;
 
     /**
      * 查询租户基础信息
@@ -151,6 +155,22 @@ public class TenantInfoServiceImpl extends ServiceImpl<TenantInfoMapper, TenantI
     public List<TenantInfoShowVo> tenantList(TenantInfo tenantInfo) {
         return baseMapper.tenantList(tenantInfo);
     }
+
+    @Override
+    public R menuChange(String id) {
+        List<SysMenu> menuList = baseMapper.selectTenantMenu();
+        List<TreeSelect> treeSelects = menuService.buildMenuTreeSelect(menuList);
+        return R.ok().put("menus", treeSelects);
+    }
+
+    @Override
+    public R menuEdit(List<Long> selected, List<Long> unSelected) {
+        // 更新选中的
+        int pitch = baseMapper.updatePitchMenu(selected);
+        // 更新未选中的
+        int unPitchMenu = baseMapper.updateUnPitchMenu(unSelected);
+        return R.ok();
+    }
 }
 
 

+ 18 - 1
fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml

@@ -45,10 +45,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="lastWatchTime"    column="last_watch_time"    />
         <result property="registerTime"    column="register_time"    />
         <result property="isReply"    column="is_reply"    />
+        <result property="applicationExternalUserId"    column="application_external_user_id"    />
     </resultMap>
 
     <sql id="selectQwExternalContactVo">
-        select id,qw_user_id,register_time,state,way_id,qw_open_user_id,stage_status,first_time,open_id,is_interact,level, unionid, user_id,transfer_time,loss_time,del_time,transfer_num, external_user_id,transfer_status,status,create_time, name, avatar, type, gender, remark, description, tag_ids, remark_mobiles, remark_corp_name, add_way, oper_userid, corp_id, company_id, company_user_id, customer_id, fs_user_id,is_reply from qw_external_contact
+        select id,qw_user_id,register_time,state,way_id,qw_open_user_id,stage_status,first_time,open_id,is_interact,level,
+               unionid, user_id,transfer_time,loss_time,del_time,transfer_num, external_user_id,transfer_status,status,
+               create_time, name, avatar, type, gender, remark, description, tag_ids, remark_mobiles,
+               remark_corp_name, add_way, oper_userid, corp_id, company_id, company_user_id, customer_id,
+               fs_user_id,is_reply,application_external_user_id from qw_external_contact
     </sql>
 
     <select id="selectQwExternalContactList" parameterType="QwExternalContact" resultMap="QwExternalContactResult">
@@ -72,6 +77,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="customerId != null "> and customer_id = #{customerId}</if>
             <if test="fsUserId != null "> and fs_user_id = #{fsUserId}</if>
             <if test="unionid != null "> and unionid = #{unionid}</if>
+            <if test="applicationExternalUserId != null and applicationExternalUserId != '' "> and application_external_user_id = #{applicationExternalUserId}</if>
         </where>
     </select>
 
@@ -277,6 +283,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="levelType != null">level_type,</if>
             <if test="firstTime != null">first_time,</if>
             <if test="registerTime != null">register_time,</if>
+            <if test="applicationExternalUserId != null">application_external_user_id,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="id != null">#{id},</if>
@@ -317,6 +324,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="levelType != null">#{levelType},</if>
             <if test="firstTime != null">#{firstTime},</if>
             <if test="registerTime != null">#{registerTime},</if>
+            <if test="applicationExternalUserId != null">#{applicationExternalUserId},</if>
          </trim>
     </insert>
 
@@ -820,4 +828,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             and t1.corp_id = #{corpId}
             and t1.status = 0
      </select>
+
+    <select id="selectQwExternalByApplicationExternalUserId" resultMap="QwExternalContactResult">
+        <include refid="selectQwExternalContactVo"/>
+        where application_external_user_id = #{externalUserid} and corp_id = #{corpId}
+        <if test="qwUserId != null and qwUserId != ''">
+            user_id = #{qwUserId}
+        </if>
+        order by create_time desc limit 1
+    </select>
 </mapper>

+ 20 - 0
fs-service/src/main/resources/mapper/tenant/TenantInfoMapper.xml

@@ -57,6 +57,12 @@
         </where>
     </select>
 
+    <select id="selectTenantMenu" resultType="com.fs.common.core.domain.entity.SysMenu">
+        select menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time
+        from sys_menu
+        order by parent_id, order_num
+    </select>
+
 
     <insert id="insertTenantInfo" parameterType="TenantInfo" useGeneratedKeys="true" keyProperty="id">
         insert into tenant_info
@@ -112,6 +118,20 @@
         where id = #{id}
     </update>
 
+    <update id="updatePitchMenu">
+        update sys_menu set visible = 0, status = 0 where menu_id in
+        <foreach item="menuId" collection="selected" open="(" separator="," close=")">
+            #{menuId}
+        </foreach>
+    </update>
+
+    <update id="updateUnPitchMenu">
+        update sys_menu set visible = 1, status = 1 where menu_id in
+        <foreach item="menuId" collection="unSelected" open="(" separator="," close=")">
+            #{menuId}
+        </foreach>
+    </update>
+
     <delete id="deleteTenantInfoById" parameterType="String">
         delete from tenant_info where id = #{id}
     </delete>