266 lines
10 KiB
Python
266 lines
10 KiB
Python
"""
|
|
企业微信回调服务器
|
|
使用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:
|
|
"""创建回调应用(用于外部集成)"""
|
|
server = get_callback_server()
|
|
return server.app
|