Browse Source

Merge remote-tracking branch 'origin/master'

yys 3 weeks ago
parent
commit
784010b28b

+ 1 - 1
fs-admin/src/main/resources/logback.xml

@@ -3,7 +3,7 @@
     <!-- 日志存放路径 -->
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-admin/logs" />
 	<property name="log.path" value="/home/fs-admin/logs" />
     <!-- 日志输出格式 -->
     <!-- 日志输出格式 -->
-    <property name="log.pattern" value="%replace([%X{traceId}] ){'\\[\\s*\\] ', ''}%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+    <property name="log.pattern" value="%replace([%X{traceId}] ){'\\[\\s*\\] ', ''}%replace([tenant:%X{tenantId}] ){'\\[tenant:\\s*\\] ', ''}%replace([ds:%X{dataSource}] ){'\\[ds:\\s*\\] ', ''}%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
 
 
 	<!-- 控制台输出 -->
 	<!-- 控制台输出 -->
 	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
 	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">

+ 1 - 1
fs-company/src/main/resources/logback.xml

@@ -3,7 +3,7 @@
     <!-- 日志存放路径 -->
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-company/logs" />
 	<property name="log.path" value="/home/fs-company/logs" />
     <!-- 日志输出格式 -->
     <!-- 日志输出格式 -->
-	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+	<property name="log.pattern" value="%replace([%X{traceId}] ){'\\[\\s*\\] ', ''}%replace([tenant:%X{tenantId}] ){'\\[tenant:\\s*\\] ', ''}%replace([ds:%X{dataSource}] ){'\\[ds:\\s*\\] ', ''}%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
 
 
 	<!-- 控制台输出 -->
 	<!-- 控制台输出 -->
 	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
 	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">

+ 18 - 0
fs-framework/src/main/java/com/fs/framework/config/LogInterceptor.java

@@ -2,6 +2,8 @@ package com.fs.framework.config;
 
 
 
 
 
 
+import com.fs.common.utils.SecurityUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
 import org.slf4j.MDC;
 import org.slf4j.MDC;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
@@ -21,6 +23,8 @@ import java.util.UUID;
 public class LogInterceptor implements HandlerInterceptor {
 public class LogInterceptor implements HandlerInterceptor {
 
 
     private static final String traceId = "traceId";
     private static final String traceId = "traceId";
+    private static final String tenantId = "tenantId";
+    private static final String dataSource = "dataSource";
 
 
     @Override
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
@@ -29,6 +33,18 @@ public class LogInterceptor implements HandlerInterceptor {
             tid = request.getHeader("traceId");
             tid = request.getHeader("traceId");
         }
         }
         MDC.put(traceId, tid);
         MDC.put(traceId, tid);
+        Long currentTenantId = SecurityUtils.getTenantId();
+        if (currentTenantId != null) {
+            MDC.put(tenantId, String.valueOf(currentTenantId));
+        } else {
+            MDC.remove(tenantId);
+        }
+        String currentDataSource = DynamicDataSourceContextHolder.getDataSourceType();
+        if (!StringUtils.isEmpty(currentDataSource)) {
+            MDC.put(dataSource, currentDataSource);
+        } else {
+            MDC.remove(dataSource);
+        }
         return true;
         return true;
     }
     }
 
 
@@ -36,5 +52,7 @@ public class LogInterceptor implements HandlerInterceptor {
     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                 Exception ex) {
                                 Exception ex) {
         MDC.remove(traceId);
         MDC.remove(traceId);
+        MDC.remove(tenantId);
+        MDC.remove(dataSource);
     }
     }
 }
 }

+ 108 - 0
fs-qw-api/src/main/java/com/fs/app/controller/QwController.java

@@ -120,4 +120,112 @@ public class QwController {
         result = "success";
         result = "success";
         return  result;
         return  result;
     }
     }
+
+    /**
+     * 功能描述:
+     * 应用详情 --> 回调配置中 --> 数据回调URL
+     */
+    @GetMapping("/qwServer/data/{corpId}")
+    public String ServerData(@PathVariable String corpId,
+                             @RequestParam("msg_signature") String msg_signature,
+                             @RequestParam("timestamp") String timestamp,
+                             @RequestParam("nonce") String nonce,
+                             @RequestParam("echostr") String echostr) {
+        logger.info("数据回调URLServer-微信调用dataGet请求");
+        return getVerifyServer(msg_signature, timestamp, nonce, echostr,corpId);
+    }
+
+    @PostMapping ("/qwServer/data/{corpId}")
+    public String ServerData(@PathVariable String corpId,
+                             @RequestBody String requestBody,
+                             @RequestParam("msg_signature") String msg_signature,
+                             @RequestParam("timestamp") String timestamp,
+                             @RequestParam("nonce") String nonce,
+                             @RequestParam(value = "corpid", required = false) String corpid) {
+        logger.info("数据回调URLServer-dataPost");
+        return qwDataCallbackService.dataCallbackServer(msg_signature,timestamp,nonce,requestBody,corpId);
+    }
+
+
+
+    public String getVerifyServer(String sVerifyMsgSig, String sVerifyTimeStamp,
+                                  String sVerifyNonce, String sVerifyEchoStr, String corpId){
+
+        QwCompany qwCompany= qwCompanyMapper.selectQwCompanyByCorpId(corpId);
+        String token = qwCompany.getToken();
+        String encodingAESKey = qwCompany.getEncodingAesKey();
+//        String corpId = qwCompanyConfig.getCorpId();
+        WXBizMsgCrypt wxcpt = null;
+        try {
+            wxcpt = new WXBizMsgCrypt(token, encodingAESKey, corpId);
+        }catch (AesException E){
+            return "error";
+        }
+        String sEchoStr; //需要返回的明文
+        try {
+            sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,
+                    sVerifyNonce, sVerifyEchoStr);
+            logger.info("企业微信验证事件需要返回的明文Server:"+sEchoStr);
+        } catch (Exception e) {
+            logger.error("企业微信验证失败Serve:"+corpId+":"+e);
+            //验证URL失败,错误原因请查看异常
+            return "error";
+        }
+        return  sEchoStr;
+    }
+
+
+
+
+
+    /**
+     *服务商-代开发应用-获取 SuiteTicket的事件回调
+     */
+    @GetMapping("/qwServerAuth/data/{corpId}")
+    public String qwServerAuthDataWGet(@RequestParam("msg_signature") String msg_signature,
+                                        @RequestParam("timestamp") String timestamp,
+                                        @RequestParam("nonce") String nonce,
+                                        @RequestParam("echostr") String echostr,
+                                        @RequestParam(value = "auth_corpid", required = false) String authCorpid,
+                                        @RequestParam(value = "corpid", required = false) String corpid,
+                                       @PathVariable String corpId) {
+        return getVerifyServerAuth(msg_signature, timestamp, nonce, echostr,corpId);
+    }
+
+    public String getVerifyServerAuth(String sVerifyMsgSig, String sVerifyTimeStamp,
+                                      String sVerifyNonce, String sVerifyEchoStr, String qwCorpID){
+
+        QwCompany qwCompany= qwCompanyMapper.selectQwCompanyByCorpId(qwCorpID);
+        String token = qwCompany.getToken();
+        String encodingAESKey = qwCompany.getEncodingAesKey();
+
+        WXBizMsgCrypt wxcpt = null;
+        try {
+            wxcpt = new WXBizMsgCrypt(token, encodingAESKey, qwCorpID);
+        }catch (AesException E){
+            return "error";
+        }
+        String sEchoStr; //需要返回的明文
+        try {
+            sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,
+                    sVerifyNonce, sVerifyEchoStr);
+            logger.info("企业微信验证事件需要返回的明文ServerAuth:"+sEchoStr);
+        } catch (Exception e) {
+            logger.error("企业微信验证失败ServeAuth:"+qwCorpID+":"+e);
+            //验证URL失败,错误原因请查看异常
+            return "error";
+        }
+        return  sEchoStr;
+    }
+
+    @PostMapping ("/qwServerAuth/data/{corpID}")
+    public String ServerAuthData(@PathVariable String corpID,
+                                 @RequestBody String requestBody,
+                                 @RequestParam("msg_signature") String msg_signature,
+                                 @RequestParam("timestamp") String timestamp,
+                                 @RequestParam("nonce") String nonce,
+                                 @RequestParam(value = "corpid", required = false) String corpid) {
+//        logger.info("数据回调URLServerAuth-dataPost");
+        return qwDataCallbackService.dataCallbackServerAuth(msg_signature,timestamp,nonce,requestBody,corpID);
+    }
 }
 }

+ 271 - 1
fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java

@@ -18,15 +18,20 @@ import com.fs.qw.param.QwAutoRulesTagsParams;
 import com.fs.qw.service.*;
 import com.fs.qw.service.*;
 import com.fs.qwApi.Result.QwGroupChatDetailsResult;
 import com.fs.qwApi.Result.QwGroupChatDetailsResult;
 import com.fs.qwApi.Result.QwOpenidResult;
 import com.fs.qwApi.Result.QwOpenidResult;
+import com.fs.qwApi.Result.QwPermanentCodeResult;
 import com.fs.qwApi.domain.QwResult;
 import com.fs.qwApi.domain.QwResult;
 import com.fs.qwApi.param.QwEditUserTagParam;
 import com.fs.qwApi.param.QwEditUserTagParam;
 import com.fs.qwApi.param.QwOpenidByUserParams;
 import com.fs.qwApi.param.QwOpenidByUserParams;
+import com.fs.qwApi.param.QwPermanentCodeParam;
+import com.fs.qwApi.param.QwSuiteTokenParams;
 import com.fs.qwApi.util.AesException;
 import com.fs.qwApi.util.AesException;
+import com.fs.qwApi.util.XMLParse;
 import com.fs.voice.utils.StringUtil;
 import com.fs.voice.utils.StringUtil;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
 import com.google.gson.JsonParser;
 import com.tencent.wework.Finance;
 import com.tencent.wework.Finance;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
 import org.json.JSONObject;
 import org.json.JSONObject;
 import org.redisson.api.RLock;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.redisson.api.RedissonClient;
@@ -37,11 +42,15 @@ import org.springframework.stereotype.Service;
 import org.w3c.dom.Document;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Element;
 import org.w3c.dom.NodeList;
 import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
 
 
 import javax.crypto.Cipher;
 import javax.crypto.Cipher;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
 import java.io.File;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.IOException;
+import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
 import java.nio.charset.StandardCharsets;
 import java.security.KeyFactory;
 import java.security.KeyFactory;
 import java.security.PrivateKey;
 import java.security.PrivateKey;
@@ -97,12 +106,125 @@ public class QwDataCallbackService {
     @Autowired
     @Autowired
     private ILeadService leadService;
     private ILeadService leadService;
 
 
+    /**
+     * 代开发模板默认 suite_id(回调 XML 里通常也会带 &lt;SuiteId&gt;,优先用 XML)
+     */
+    private static final String DEFAULT_SUITE_ID = "dk348a71ab9e97e43a";
+    /**
+     * 代开发模板 suite_secret(与服务商后台一致)
+     */
+    private static final String DEFAULT_SUITE_SECRET = "bUdYYGwWPSz4Kg11O5qQfrsIFwlGRh4W-JsJrlVy3A0";
+
+    /**
+     * 使用临时授权码 auth_code 调用 get_permanent_code,回写当前 qw_company 的 permanent_code。
+     * 适用于:首次安装 create_auth、重置永久授权码 reset_permanent_code 等带 AuthCode 的指令回调。
+     */
+    private void syncPermanentCodeByAuthCode(QwCompany qwCompany, String corpId, String authCode, String suiteIdFromXml) {
+        if (StringUtils.isBlank(authCode)) {
+            logger.warn("syncPermanentCodeByAuthCode authCode 为空, corpId={}", corpId);
+            return;
+        }
+        String effectiveSuiteId = StringUtils.isNotBlank(suiteIdFromXml) ? suiteIdFromXml.trim() : DEFAULT_SUITE_ID;
+
+        QwSuiteTokenParams qwSuiteTokenParams = new QwSuiteTokenParams();
+        qwSuiteTokenParams.setSuite_id(effectiveSuiteId);
+        qwSuiteTokenParams.setSuite_secret(DEFAULT_SUITE_SECRET);
+        String cacheKey = "qw_suite_ticket:" + corpId;
+        String suiteTicket = redisCache.getCacheObject(cacheKey);
+        if (StringUtils.isBlank(suiteTicket)) {
+            logger.error("syncPermanentCodeByAuthCode 未找到 suite_ticket(Redis key={}),无法换取 suite_access_token,请确认 suite_ticket 回调已正常写入", cacheKey);
+            return;
+        }
+        qwSuiteTokenParams.setSuite_ticket(suiteTicket);
+
+        String suiteAccessToken = qwApiService.getSuiteToken(qwSuiteTokenParams);
+        if (StringUtils.isBlank(suiteAccessToken)) {
+            logger.error("syncPermanentCodeByAuthCode suite_access_token 为空, corpId={}, suiteId={}", corpId, effectiveSuiteId);
+            return;
+        }
+
+        QwPermanentCodeParam qwPermanentCodeParam = new QwPermanentCodeParam();
+        qwPermanentCodeParam.setAuth_code(authCode.trim());
+        QwPermanentCodeResult permanentCode = qwApiService.getPermanentCode(qwPermanentCodeParam, suiteAccessToken);
+        if (permanentCode == null) {
+            logger.error("getPermanentCode 返回 null, corpId={}", corpId);
+            return;
+        }
+        if (permanentCode.getErrcode() != 0) {
+            logger.error("getPermanentCode 失败 corpId={}, errcode={}, errmsg={}", corpId, permanentCode.getErrcode(), permanentCode.getErrmsg());
+            return;
+        }
+        if (StringUtils.isBlank(permanentCode.getPermanent_code())) {
+            logger.error("getPermanentCode 成功但 permanent_code 为空, corpId={}", corpId);
+            return;
+        }
+
+        QwCompany map = new QwCompany();
+        map.setId(qwCompany.getId());
+        map.setPermanentCode(permanentCode.getPermanent_code());
+        int rows = qwCompanyMapper.updateQwCompany(map);
+        logger.info("已更新 qw_company 永久授权码, corpId={}, qwCompanyId={}, rows={}", corpId, qwCompany.getId(), rows);
+    }
+
     @Async
     @Async
     public void dataCallback( Document document,String corpId,QwCompany qwCompany) throws Exception {
     public void dataCallback( Document document,String corpId,QwCompany qwCompany) throws Exception {
 
 
 
 
         Element root = document.getDocumentElement();
         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;
+            }
+        }
+
+        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");
         NodeList msgTypeNode = root.getElementsByTagName("MsgType");
+
+        // 如果连 MsgType 都没有,说明是其他类型回调,直接返回
+        if (msgTypeNode.getLength() == 0) {
+            logger.error("未知回调类型,无 MsgType");
+            return;
+        }
+
         String msgType = msgTypeNode.item(0).getTextContent();
         String msgType = msgTypeNode.item(0).getTextContent();
         if(msgType.equals("event")){
         if(msgType.equals("event")){
             NodeList eventNode = root.getElementsByTagName("Event");
             NodeList eventNode = root.getElementsByTagName("Event");
@@ -147,7 +269,7 @@ public class QwDataCallbackService {
                         qwUser.setCorpId(corpId);
                         qwUser.setCorpId(corpId);
 
 
                         if (qw==null){
                         if (qw==null){
-                            String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), userID);
+                            String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), userID,qwCompany.getPermanentCode());
                             qwUser.setQwUserId(userID);
                             qwUser.setQwUserId(userID);
                             qwUser.setDepartment(root.getElementsByTagName("Department").item(0).getTextContent().toString());
                             qwUser.setDepartment(root.getElementsByTagName("Department").item(0).getTextContent().toString());
                             qwUser.setQwUserName(serverQwUserName);
                             qwUser.setQwUserName(serverQwUserName);
@@ -874,4 +996,152 @@ public class QwDataCallbackService {
         return s;
         return s;
     }
     }
 
 
+    public String dataCallbackServerAuth(String sVerifyMsgSig,String sVerifyTimeStamp,String sVerifyNonce,String sData,String qwCorpID) {
+
+        QwCompany qwCompany= qwCompanyMapper.selectQwCompanyByCorpId(qwCorpID);
+        String token = qwCompany.getToken();
+        String encodingAESKey = qwCompany.getEncodingAesKey();
+        String result = "error";
+        com.fs.qwApi.util.WXBizMsgCrypt wxcpt = null;
+        try {
+            Object[] encrypt = XMLParse.extract(sData);
+            String sSuiteid = (String) encrypt[2];
+            wxcpt = new com.fs.qwApi.util.WXBizMsgCrypt(token, encodingAESKey, sSuiteid);
+        }catch (AesException E){
+            return result;
+        }
+        try{
+            String sMsg = wxcpt.DecryptMsg(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sData);
+//            logger.info("企业微信回调事件:ServerAuth"+sMsg);
+            // 加密成功
+            // TODO: 解析出明文xml标签的内容进行处理
+            // For example:
+            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+            DocumentBuilder db = dbf.newDocumentBuilder();
+            StringReader sr = new StringReader(sMsg);
+            InputSource is = new InputSource(sr);
+            Document document = db.parse(is);
+
+            Element root = document.getDocumentElement();
+            String msgType = root.getElementsByTagName("InfoType").item(0).getTextContent();
+
+            //10分钟推送一次的suite_ticket
+            if(msgType.equals("suite_ticket")){
+
+                String SuiteTicket =root.getElementsByTagName("SuiteTicket").item(0).getTextContent();
+                String SuiteId =root.getElementsByTagName("SuiteId").item(0).getTextContent();
+
+                switch (SuiteId) {
+                    case "dk348a71ab9e97e43a":
+                        //代开发应用模板信息
+                        redisCache.setCacheObject("qw_suite_ticket:" + qwCorpID,SuiteTicket);
+                        break;
+                    case "ww0551047d6db89f0a":
+                        //登录授权
+//                        redisTemplate.opsForValue().set("qwServer:signServer_suite_ticket",SuiteTicket);
+                        redisCache.setCacheObject("qwServer:signServer_suite_ticket",SuiteTicket);
+                        break;
+                    case "ww378fbd25f8e3e9f8":
+                        // 通讯录编辑授权
+//                        redisTemplate.opsForValue().set("qwServer:bookServer_suite_ticket",SuiteTicket);
+                        redisCache.setCacheObject("qwServer:bookServer_suite_ticket",SuiteTicket);
+                        break;
+                    default:
+                        break;
+                }
+            }
+            //取消授权事件(先放着)
+            if (msgType.equals("cancel_auth")){
+//
+//                String SuiteId = root.getElementsByTagName("SuiteId").item(0).getTextContent();
+//                String AuthCorpId = root.getElementsByTagName("AuthCorpId").item(0).getTextContent();
+            }
+            //授权
+            if(msgType.equals("create_auth")||msgType.equals("reset_permanent_code")){
+                //第三方应用的SuiteId或者代开发应用模板id
+                String SuiteId = root.getElementsByTagName("SuiteId").item(0).getTextContent();
+                //临时授权码
+                String AuthCode  = root.getElementsByTagName("AuthCode").item(0).getTextContent();
+
+                QwSuiteTokenParams qwSuiteTokenParams=new QwSuiteTokenParams();
+
+                switch (SuiteId) {
+                    case "dk348a71ab9e97e43a":
+                        //代开发应用模板信息
+                        NodeList suiteIdNodes = root.getElementsByTagName("SuiteId");
+                        String suiteIdXml = suiteIdNodes.getLength() > 0 ? suiteIdNodes.item(0).getTextContent() : null;
+                        syncPermanentCodeByAuthCode(qwCompany, qwCorpID, AuthCode, suiteIdXml);
+                        break;
+                    case "ww0551047d6db89f0a":
+                        //登录授权(暂时不做)
+//                        String signServer_suite_ticket = (String) redisTemplate.opsForValue().get("qwServer:signServer_suite_ticket");
+
+                        break;
+                    case "ww378fbd25f8e3e9f8":
+                        // 通讯录编辑授权
+//                        String bookServer_suite_ticket = (String) redisTemplate.opsForValue().get("qwServer:bookServer_suite_ticket");
+                        String bookServer_suite_ticket = (String) redisCache.getCacheObject("qwServer:bookServer_suite_ticket");
+
+                        QwCompany qwBookServerConfig= qwCompanyMapper.selectQwCompanyByCorpId(qwCorpID);
+
+                        qwSuiteTokenParams.setSuite_ticket(bookServer_suite_ticket);
+//                        qwSuiteTokenParams.setSuite_id(qwBookServerConfig.getSuiteId());
+//                        qwSuiteTokenParams.setSuite_secret(qwBookServerConfig.getSecret());
+//
+//                        String bookSuiteToken = qwApiService.getSuiteToken(qwSuiteTokenParams);
+//                        //临时授权码
+//                        QwPermanentCodeParam  qwBookPermanentCodeParam=new QwPermanentCodeParam();
+//                        qwBookPermanentCodeParam.setAuth_code(AuthCode);
+//
+//                        //通过临时授权码以及代开发应用通用suite_ticket获取永久授权码及授权的企业信息
+//                        QwPermanentCodeResult bookPermanentCode = qwApiService.getPermanentCode(qwBookPermanentCodeParam, bookSuiteToken);
+//
+//                        //密文企业ID
+//                        String bookCorpid = bookPermanentCode.getAuth_corp_info().getCorpid();
+//
+//                        //各自的企业微信配置
+////                        CompanyConfig bookConfig = companyConfigService.selectCompanyConfigByServerKeyAndCorpid("sys:qw:config",bookCorpid);
+//                        QwCompany qwBookConfig= qwCompanyMapper.selectQwCompanyByCorpId(bookCorpid);
+//
+//                        //永久授权码
+//                        qwBookConfig.setPermanentCode(bookPermanentCode.getPermanent_code());
+//
+//                        qwCompanyMapper.updateQwCompany(qwBookConfig);
+
+                        break;
+                    default:
+                        break;
+                }
+            }
+//            if(msgType.equals("auth_external_contact")){
+//                NodeList AdminMobileLocationList = root.getElementsByTagName("AdminMobileLocation");
+//                NodeList SuiteIdList = root.getElementsByTagName("SuiteId");
+//                NodeList AuthExternalContactCodeList = root.getElementsByTagName("AuthExternalContactCode");
+//                NodeList AuthCorpIdList = root.getElementsByTagName("AuthCorpId");
+//                String AdminMobileLocation = AdminMobileLocationList.item(0).getTextContent();
+//                String SuiteId = SuiteIdList.item(0).getTextContent();
+//                String AuthExternalContactCode = AuthExternalContactCodeList.item(0).getTextContent();
+//                String AuthCorpId = AuthCorpIdList.item(0).getTextContent();
+//
+//                logger.info("AdminMobileLocationServerAuth:"+AdminMobileLocation);
+//                logger.info("SuiteIdServerAuth:"+SuiteId);
+//                logger.info("AuthExternalContactCodeServerAuth:"+AuthExternalContactCode);
+//                logger.info("AuthCorpIdServerAuth:"+AuthCorpId);
+//            }
+
+        }
+        catch(Exception e)
+        {
+            e.printStackTrace();
+            // 加密失败
+            logger.error("加密失败:"+result);
+            return result;
+        }
+        result = "success";
+        return  result;
+    }
+
+    public String dataCallbackServer(String msgSignature, String timestamp, String nonce, String requestBody, String corpId) {
+        return "";
+    }
 }
 }

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

@@ -96,4 +96,9 @@ public class QwCompany extends BaseEntity
     private String shareAgentId;
     private String shareAgentId;
 
 
     private String shareSchema;
     private String shareSchema;
+
+    /**
+     * 服务商永久授权码
+     */
+    private String permanentCode;
 }
 }

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

@@ -16,6 +16,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.PubFun;
+import com.fs.common.utils.SecurityUtils;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyConfigService;
 import com.fs.company.service.ICompanyConfigService;
@@ -55,6 +56,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
@@ -935,7 +937,7 @@ public class QwUserServiceImpl implements IQwUserService
         QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
         QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
         for (DeptUser user : deptUser) {
         for (DeptUser user : deptUser) {
             QwUser qw=qwUserMapper.selectQwUserByCorpIdAndUserId(corpId,user.getUserid());
             QwUser qw=qwUserMapper.selectQwUserByCorpIdAndUserId(corpId,user.getUserid());
-            String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), user.getUserid());
+            String serverQwUserName = qwApiService.getServerQwUserName(corpId, qwCompany.getOpenSecret(), user.getUserid(),qwCompany.getPermanentCode());
             log.info("同步用户名称:{}", serverQwUserName);
             log.info("同步用户名称:{}", serverQwUserName);
             QwUser qwUser = new QwUser();
             QwUser qwUser = new QwUser();
             qwUser.setQwUserId(user.getUserid());
             qwUser.setQwUserId(user.getUserid());
@@ -971,11 +973,55 @@ public class QwUserServiceImpl implements IQwUserService
     public R syncQwUserName(String corpId) {
     public R syncQwUserName(String corpId) {
         QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
         QwCompany qwCompany = iQwCompanyService.selectQwCompanyByCorpId(corpId);
         List<QwUser> qwUsers=qwUserMapper.selectQwUserByCorpId(corpId);
         List<QwUser> qwUsers=qwUserMapper.selectQwUserByCorpId(corpId);
+        if (qwCompany == null) {
+            log.warn("syncQwUserName skipped: corpId={} 对应企业为空", corpId);
+            return R.error("企业信息不存在");
+        }
+        if (qwUsers == null || qwUsers.isEmpty()) {
+            log.info("syncQwUserName no users: corpId={}", corpId);
+            return R.ok();
+        }
+        String currentDataSource = getCurrentDataSourceType();
+        Long tenantId = SecurityUtils.getTenantId();
+        String currentTraceId = MDC.get("traceId");
+        log.info("syncQwUserName start: corpId={}, tenantId={}, dataSource={}, userCount={}",
+                corpId, tenantId, currentDataSource, qwUsers.size());
         ExecutorService executorService = Executors.newFixedThreadPool(50);
         ExecutorService executorService = Executors.newFixedThreadPool(50);
         for (QwUser user : qwUsers) {
         for (QwUser user : qwUsers) {
-            executorService.submit(() -> updateQwUserName(user, qwCompany));
+            executorService.submit(() -> {
+                try {
+                    if (currentDataSource != null && !currentDataSource.isEmpty()) {
+                        setCurrentDataSourceType(currentDataSource);
+                        MDC.put("dataSource", currentDataSource);
+                    }
+                    if (tenantId != null) {
+                        MDC.put("tenantId", String.valueOf(tenantId));
+                    }
+                    if (currentTraceId != null && !currentTraceId.isEmpty()) {
+                        MDC.put("traceId", currentTraceId);
+                    }
+                    updateQwUserName(user, qwCompany);
+                } finally {
+                    clearCurrentDataSourceType();
+                    MDC.remove("dataSource");
+                    MDC.remove("tenantId");
+                    MDC.remove("traceId");
+                }
+            });
         }
         }
         executorService.shutdown(); // 关闭线程池,等待所有任务完成
         executorService.shutdown(); // 关闭线程池,等待所有任务完成
+        try {
+            if (!executorService.awaitTermination(5, TimeUnit.MINUTES)) {
+                log.warn("syncQwUserName timeout: corpId={}, tenantId={}, dataSource={}",
+                        corpId, tenantId, currentDataSource);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("syncQwUserName interrupted: corpId={}, tenantId={}, dataSource={}",
+                    corpId, tenantId, currentDataSource, e);
+        }
+        log.info("syncQwUserName finish: corpId={}, tenantId={}, dataSource={}",
+                corpId, tenantId, currentDataSource);
         return R.ok();
         return R.ok();
     }
     }
 
 
@@ -985,17 +1031,53 @@ public class QwUserServiceImpl implements IQwUserService
     }
     }
 
 
     void updateQwUserName(QwUser user,QwCompany qwCompany){
     void updateQwUserName(QwUser user,QwCompany qwCompany){
-        String serverQwUserName = qwApiService.getServerQwUserName(qwCompany.getCorpId(), qwCompany.getOpenSecret(), user.getQwUserId());
+        String serverQwUserName = qwApiService.getServerQwUserName(qwCompany.getCorpId(), qwCompany.getOpenSecret(), user.getQwUserId(),qwCompany.getPermanentCode());
         if (serverQwUserName==null|| serverQwUserName.isEmpty()){
         if (serverQwUserName==null|| serverQwUserName.isEmpty()){
+            log.debug("syncQwUserName skip empty: corpId={}, userId={}", qwCompany.getCorpId(), user.getQwUserId());
             return;
             return;
         }
         }
         if (serverQwUserName.equals(user.getQwUserName())){
         if (serverQwUserName.equals(user.getQwUserName())){
+            log.debug("syncQwUserName skip same: corpId={}, userId={}, name={}",
+                    qwCompany.getCorpId(), user.getQwUserId(), serverQwUserName);
             return;
             return;
         }
         }
         QwUser qwUser = new QwUser();
         QwUser qwUser = new QwUser();
         qwUser.setId(user.getId());
         qwUser.setId(user.getId());
         qwUser.setQwUserName (serverQwUserName);
         qwUser.setQwUserName (serverQwUserName);
-        qwUserMapper.updateQwUser(qwUser);
+        int updateRows = qwUserMapper.updateQwUser(qwUser);
+        log.info("syncQwUserName update result: corpId={}, userId={}, oldName={}, newName={}, rows={}",
+                qwCompany.getCorpId(), user.getQwUserId(), user.getQwUserName(), serverQwUserName, updateRows);
+    }
+
+    private String getCurrentDataSourceType() {
+        try {
+            Class<?> holderClass = Class.forName("com.fs.framework.datasource.DynamicDataSourceContextHolder");
+            Object value = holderClass.getMethod("getDataSourceType").invoke(null);
+            return value == null ? null : String.valueOf(value);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private void setCurrentDataSourceType(String dataSourceType) {
+        if (dataSourceType == null || dataSourceType.isEmpty()) {
+            return;
+        }
+        try {
+            Class<?> holderClass = Class.forName("com.fs.framework.datasource.DynamicDataSourceContextHolder");
+            holderClass.getMethod("setDataSourceType", String.class).invoke(null, dataSourceType);
+        } catch (Exception e) {
+            log.debug("setCurrentDataSourceType skipped: {}", dataSourceType);
+        }
+    }
+
+    private void clearCurrentDataSourceType() {
+        try {
+            Class<?> holderClass = Class.forName("com.fs.framework.datasource.DynamicDataSourceContextHolder");
+            holderClass.getMethod("clearDataSourceType").invoke(null);
+        } catch (Exception e) {
+            // ignore
+        }
     }
     }
 
 
     @Override
     @Override

+ 9 - 1
fs-service/src/main/java/com/fs/qwApi/config/QwApiConfig.java

@@ -26,13 +26,19 @@ public interface QwApiConfig {
     * 服务商-获取企业永久授权码/代开发授权应用secret的获取
     * 服务商-获取企业永久授权码/代开发授权应用secret的获取
      * 根据auth_code和suite_access_token获取到corpid和secret(permanent_code)永久的 用于生成正常的access_token
      * 根据auth_code和suite_access_token获取到corpid和secret(permanent_code)永久的 用于生成正常的access_token
     */
     */
-    String getPermanentCode="https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code";
+    String getPermanentCode="https://qyapi.weixin.qq.com/cgi-bin/service/v2/get_permanent_code";
+//    String getPermanentCode="https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code";
 
 
     /**
     /**
     * 服务商-获取通讯录的token
     * 服务商-获取通讯录的token
     */
     */
     String getCorp_token="https://qyapi.weixin.qq.com/cgi-bin/service/get_corp_token";
     String getCorp_token="https://qyapi.weixin.qq.com/cgi-bin/service/get_corp_token";
 
 
+    /**
+     * 服务商-获取预授权码
+     */
+    String getPre_auth_code="https://qyapi.weixin.qq.com/cgi-bin/service/get_pre_auth_code";
+
     /**
     /**
     * 服务商-读取成员(获取昵称)
     * 服务商-读取成员(获取昵称)
     */
     */
@@ -331,4 +337,6 @@ public interface QwApiConfig {
     String sendMsg="https://qyapi.weixin.qq.com/cgi-bin/message/send";
     String sendMsg="https://qyapi.weixin.qq.com/cgi-bin/message/send";
 
 
     String openidToExternalUserid="https://qyapi.weixin.qq.com/cgi-bin/corpgroup/unionid_to_external_userid";
     String openidToExternalUserid="https://qyapi.weixin.qq.com/cgi-bin/corpgroup/unionid_to_external_userid";
+
+    String useridToOpenUserid="https://qyapi.weixin.qq.com/cgi-bin/batch/userid_to_openuserid";
 }
 }

+ 3 - 1
fs-service/src/main/java/com/fs/qwApi/service/QwApiService.java

@@ -213,6 +213,8 @@ public interface QwApiService {
     */
     */
     QwOpenidResult externalcontactToOpenid(QwOpenidByExternalcontactParams param,String corpId);
     QwOpenidResult externalcontactToOpenid(QwOpenidByExternalcontactParams param,String corpId);
 
 
+    String getOpenUserid(String accessToken,String userId);
+
     String getToken(String corpId,String corpSecret);
     String getToken(String corpId,String corpSecret);
 
 
 
 
@@ -246,7 +248,7 @@ public interface QwApiService {
     /**
     /**
     * 服务商-读取员共信息 (通过userid)
     * 服务商-读取员共信息 (通过userid)
     */
     */
-    String getServerQwUserName(String corpId,String Secret,String userid);
+    String getServerQwUserName(String corpId,String Secret,String userid,String permanentCode);
 
 
     /**
     /**
      *  获取部门列表
      *  获取部门列表

+ 40 - 4
fs-service/src/main/java/com/fs/qwApi/service/impl/QwApiServiceImpl.java

@@ -918,6 +918,41 @@ public class QwApiServiceImpl implements QwApiService {
         return JSON.parseObject(json, QwOpenidResult.class);
         return JSON.parseObject(json, QwOpenidResult.class);
     }
     }
 
 
+    @Override
+    public String getOpenUserid(String accessToken,String userId) {
+        JSONObject json=new JSONObject();
+        HttpClient httpClient = HttpClients.createDefault();
+        try {
+            URIBuilder builder = new URIBuilder(QwApiConfig.useridToOpenUserid);
+            builder.setParameter("access_token", accessToken);
+            URI uri = builder.build();
+            HttpPost httpPost  = new HttpPost(uri);
+            // 设置请求体
+            ArrayList<String> list = new ArrayList<>();
+            list.add(userId);
+            json.put("userid_list", list);
+            httpPost.setEntity( new StringEntity(JSON.toJSONString(json),StandardCharsets.UTF_8));
+            HttpResponse response = httpClient.execute(httpPost);
+            String reJson = EntityUtils.toString(response.getEntity());
+            JSONObject jsonObject = JSON.parseObject(reJson);
+            String openUserIdListStr = jsonObject.getString("open_userid_list");
+            if (StringUtils.isNotBlank(openUserIdListStr)) {
+                List<HashMap> openUserIdList = JSONObject.parseArray(openUserIdListStr, HashMap.class);
+                if (!openUserIdList.isEmpty()) {
+                    for (HashMap map : openUserIdList) {
+                        if (map.get("userid").toString().equals(userId)) {
+                            return map.get("open_userid").toString();
+                        }
+                    }
+                }
+
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
 
 
 
 
     @Override
     @Override
@@ -1083,14 +1118,15 @@ public class QwApiServiceImpl implements QwApiService {
      * 服务商-读取员共信息 (通过userid)
      * 服务商-读取员共信息 (通过userid)
      */
      */
     @Override
     @Override
-    public String getServerQwUserName(String corpId,String Secret, String userid) {
-
+    public String getServerQwUserName(String corpId,String Secret, String userid,String permanentCode) {
         HttpClient httpClient = HttpClients.createDefault();
         HttpClient httpClient = HttpClients.createDefault();
         String name = "";
         String name = "";
         try {
         try {
             URIBuilder builder = new URIBuilder(QwApiConfig.getServerQwUserName);
             URIBuilder builder = new URIBuilder(QwApiConfig.getServerQwUserName);
-            builder.setParameter("access_token", getToken(corpId,Secret));
-            builder.setParameter("userid", userid);
+            String accessToken = getToken(corpId, permanentCode);
+            builder.setParameter("access_token", accessToken);
+            String transUserId = getOpenUserid(accessToken,userid);
+            builder.setParameter("userid", transUserId);
             URI uri = builder.build();
             URI uri = builder.build();
             HttpPost httpPost  = new HttpPost(uri);
             HttpPost httpPost  = new HttpPost(uri);
             HttpResponse response = httpClient.execute(httpPost);
             HttpResponse response = httpClient.execute(httpPost);

+ 3 - 3
fs-service/src/main/java/com/fs/qwApi/util/WXBizMsgCrypt.java

@@ -193,9 +193,9 @@ public class WXBizMsgCrypt {
 		}
 		}
 
 
 		// corpid不相同的情况
 		// corpid不相同的情况
-		if (!from_corpid.equals(corpId)) {
-			throw new AesException(AesException.ValidateCorpidError);
-		}
+//		if (!from_corpid.equals(corpId)) {
+//			throw new AesException(AesException.ValidateCorpidError);
+//		}
 		return xmlContent;
 		return xmlContent;
 
 
 	}
 	}

+ 6 - 1
fs-service/src/main/resources/mapper/qw/QwCompanyMapper.xml

@@ -33,10 +33,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="shareAppId"    column="share_app_id"    />
         <result property="shareAppId"    column="share_app_id"    />
         <result property="shareAgentId"    column="share_agent_id"    />
         <result property="shareAgentId"    column="share_agent_id"    />
         <result property="shareSchema"    column="share_schema"    />
         <result property="shareSchema"    column="share_schema"    />
+        <result property="permanentCode"    column="permanent_code"    />
     </resultMap>
     </resultMap>
 
 
     <sql id="selectQwCompanyVo">
     <sql id="selectQwCompanyVo">
-        select id, corp_id, corp_name, open_secret, open_corp_id, server_agent_id, server_book_corp_id, server_book_secret, token, encoding_aes_key, provider_secret, realm_name_url, notify_url, chat_toolbar, chat_toolbar_oauth, company_ids, status, create_time, update_time, create_by,is_buy,mini_app_id,company_server_num,share_app_id,share_agent_id,share_schema from qw_company
+        select id, corp_id, corp_name, open_secret, open_corp_id, server_agent_id, server_book_corp_id,
+               server_book_secret, token, encoding_aes_key, provider_secret, realm_name_url, notify_url,
+               chat_toolbar, chat_toolbar_oauth, company_ids, status, create_time, update_time, create_by,
+               is_buy,mini_app_id,company_server_num,share_app_id,share_agent_id,share_schema,permanent_code from qw_company
     </sql>
     </sql>
 
 
     <select id="selectQwCompanyList" parameterType="QwCompany" resultMap="QwCompanyResult">
     <select id="selectQwCompanyList" parameterType="QwCompany" resultMap="QwCompanyResult">
@@ -168,6 +172,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="shareAppId != null">share_app_id = #{shareAppId},</if>
             <if test="shareAppId != null">share_app_id = #{shareAppId},</if>
             <if test="shareAgentId != null">share_agent_id = #{shareAgentId},</if>
             <if test="shareAgentId != null">share_agent_id = #{shareAgentId},</if>
             <if test="shareSchema != null">share_schema = #{shareSchema},</if>
             <if test="shareSchema != null">share_schema = #{shareSchema},</if>
+            <if test="permanentCode != null">permanent_code = #{permanentCode},</if>
         </trim>
         </trim>
         where id = #{id}
         where id = #{id}
     </update>
     </update>