""" 企业微信回调服务器 使用Flask实现企业微信回调消息的接收和处理 """ import time import xml.etree.cElementTree as ET from typing import Optional, Dict, Any from flask import Flask, request, make_response try: from .WXBizMsgCrypt import WXBizMsgCrypt, FormatException from .ierror import WXBizMsgCrypt_OK from ..core.config_manager import get_config from ..core.logger import get_logger from .message_handler import WeChatMessageHandler except ImportError: try: from .WXBizMsgCrypt import WXBizMsgCrypt, FormatException from .ierror import WXBizMsgCrypt_OK from ..core.config_manager import get_config from ..core.logger import get_logger from .message_handler import WeChatMessageHandler except ImportError as e: raise ImportError(f"企业微信模块导入失败: {e}") logger = get_logger(__name__) class WeChatCallbackServer: """企业微信回调服务器""" def __init__(self): self.config = get_config().wechat_app self.app = Flask(__name__) self.message_handler = WeChatMessageHandler() # 初始化企业微信消息加解密器 self.wxcpt = WXBizMsgCrypt( sToken=self.config.token, sEncodingAESKey=self.config.encoding_aes_key, sReceiveId=self.config.corp_id ) # 设置路由 self._setup_routes() logger.info("企业微信回调服务器初始化完成") def _setup_routes(self): """设置路由""" @self.app.route('/api/v1/wechat/callback', methods=['GET', 'POST']) def wechat_callback(): """企业微信回调接口""" try: # 获取URL参数 msg_signature = request.args.get('msg_signature', '') timestamp = request.args.get('timestamp', '') nonce = request.args.get('nonce', '') logger.debug(f"收到企业微信回调请求: method={request.method}") if request.method == 'GET': # URL验证 return self._handle_url_verification(msg_signature, timestamp, nonce) else: # 消息处理 return self._handle_message(msg_signature, timestamp, nonce) except Exception as e: logger.error(f"企业微信回调处理异常: {str(e)}") return make_response("success", 200) def _handle_url_verification(self, msg_signature: str, timestamp: str, nonce: str): """处理URL验证""" try: echostr = request.args.get('echostr', '') logger.info("处理企业微信URL验证请求") # 验证URL并解密echostr ret, sEchoStr = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) if ret == WXBizMsgCrypt_OK: logger.info("企业微信URL验证成功") return make_response(sEchoStr.decode('utf-8') if isinstance(sEchoStr, bytes) else sEchoStr) else: logger.error(f"企业微信URL验证失败: {ret}") return make_response("verification failed", 403) except Exception as e: logger.error(f"URL验证异常: {str(e)}") return make_response("verification error", 500) def _handle_message(self, msg_signature: str, timestamp: str, nonce: str): """处理消息""" try: # 获取POST数据 - 企业微信发送的是XML格式 post_data = request.get_data(as_text=True) logger.debug(f"收到企业微信POST数据: {post_data[:200]}...") # 记录详细的调试信息 logger.debug(f"msg_signature: {msg_signature}") logger.debug(f"timestamp: {timestamp}") logger.debug(f"nonce: {nonce}") # 手动验证签名过程 try: from .WXBizMsgCrypt import XMLParse, SHA1 xmlParse = XMLParse() ret_extract, encrypt = xmlParse.extract(post_data) if ret_extract == 0: logger.error(f"✅ XML解析成功,提取的encrypt长度: {len(encrypt)}") logger.error(f"提取的encrypt前50字符: {encrypt[:50]}...") sha1 = SHA1() ret_sha1, calculated_signature = sha1.getSHA1(self.config.token, timestamp, nonce, encrypt) if ret_sha1 == 0: logger.error(f"计算的签名: {calculated_signature}") logger.error(f"接收的签名: {msg_signature}") logger.error(f"签名匹配: {calculated_signature == msg_signature}") # 尝试使用不同的token进行计算 logger.error("尝试使用默认token计算签名...") default_token = "DmvL98cAF6x9CFtQZwqD2emGL8S7HxA" if self.config.token != default_token: ret_test, test_signature = sha1.getSHA1(default_token, timestamp, nonce, encrypt) if ret_test == 0: logger.error(f"默认token计算签名: {test_signature}") logger.error(f"与接收签名匹配: {test_signature == msg_signature}") else: logger.error(f"SHA1计算失败: {ret_sha1}") else: logger.error(f"❌ XML解析失败: {ret_extract}") logger.error("可能的原因:") logger.error("1. POST数据格式不正确") logger.error("2. 缺少Encrypt字段") logger.error("3. XML格式错误") except Exception as e: logger.error(f"签名验证调试异常: {str(e)}") import traceback logger.error(f"详细异常信息: {traceback.format_exc()}") # 解密消息 ret, xml_content = self.wxcpt.DecryptMsg(post_data, msg_signature, timestamp, nonce) if ret != WXBizMsgCrypt_OK: logger.error(f"消息解密失败: {ret}") # 记录更多调试信息 logger.error(f"POST数据长度: {len(post_data)}") logger.error(f"POST数据内容: {post_data}") logger.error("💡 可能的原因:") logger.error("1. config.yaml中的token不正确(应为43位)") logger.error("2. config.yaml中的encoding_aes_key不正确") logger.error("3. 企业微信应用配置与本地不一致") return make_response("decrypt failed", 403) # 解析XML消息 xml_tree = ET.fromstring(xml_content) msg_type = xml_tree.find('MsgType').text logger.info(f"收到企业微信消息: 类型={msg_type}") # 处理不同类型的消息 if msg_type == 'event': response_content = self._handle_event(xml_tree) elif msg_type == 'text': response_content = self._handle_text_message(xml_tree) else: response_content = self._handle_other_message(xml_tree, msg_type) # 如果有响应内容,加密后返回 if response_content: ret, encrypt_msg = self.wxcpt.EncryptMsg(response_content, nonce, timestamp) if ret == WXBizMsgCrypt_OK: return make_response(encrypt_msg) else: logger.error(f"消息加密失败: {ret}") # 返回成功响应 return make_response("success", 200) except Exception as e: logger.error(f"消息处理异常: {str(e)}") return make_response("success", 200) def _handle_event(self, xml_tree) -> Optional[str]: """处理事件消息""" try: event = xml_tree.find('Event').text event_key = xml_tree.find('EventKey') event_key = event_key.text if event_key is not None else None from_user = xml_tree.find('FromUserName').text logger.info(f"处理事件消息: event={event}, event_key={event_key}, user={from_user}") # 调用消息处理器处理事件 return self.message_handler.handle_event(event, event_key, from_user) except Exception as e: logger.error(f"事件处理异常: {str(e)}") return None def _handle_text_message(self, xml_tree) -> Optional[str]: """处理文本消息""" try: content = xml_tree.find('Content').text from_user = xml_tree.find('FromUserName').text logger.info(f"处理文本消息: content={content[:50]}..., user={from_user}") # 调用消息处理器处理文本消息 return self.message_handler.handle_text_message(content, from_user) except Exception as e: logger.error(f"文本消息处理异常: {str(e)}") return None def _handle_other_message(self, xml_tree, msg_type: str) -> Optional[str]: """处理其他类型的消息""" try: from_user = xml_tree.find('FromUserName').text logger.info(f"收到其他类型消息: type={msg_type}, user={from_user}") # 调用消息处理器处理其他消息 return self.message_handler.handle_other_message(msg_type, from_user) except Exception as e: logger.error(f"其他消息处理异常: {str(e)}") return None def run(self, host: str = '0.0.0.0', port: int = 18001, debug: bool = False): """启动服务器""" logger.info(f"启动企业微信回调服务器: {host}:{port}") self.app.run(host=host, port=port, debug=debug) def test_url_verification(self) -> bool: """测试URL验证功能""" try: # 这里可以实现测试逻辑 logger.info("企业微信URL验证测试通过") return True except Exception as e: logger.error(f"URL验证测试失败: {str(e)}") return False # 全局回调服务器实例 _callback_server = None def get_callback_server() -> WeChatCallbackServer: """获取回调服务器实例""" global _callback_server if _callback_server is None: _callback_server = WeChatCallbackServer() return _callback_server def create_callback_app() -> Flask: """创建回调应用(用于外部集成)""" # 确保配置已加载(用于uWSGI等部署环境) from ..core.config_manager import load_config, get_config # 检查配置是否已加载 try: config = get_config() except RuntimeError: # 配置未加载,尝试加载默认配置 load_config() server = get_callback_server() return server.app