Program/mooc/utils/wechat_client.py

224 lines
10 KiB
Python
Raw Normal View History

2024-12-31 22:27:04 +08:00
import json
import httpx
from typing import Optional, Dict, Any, Union
2024-12-31 22:27:04 +08:00
from mooc.core.config import settings
import asyncio
import time
import logging
import os
logger = logging.getLogger(__name__)
2024-12-31 22:27:04 +08:00
class WeChatAPI:
2024-12-31 22:27:04 +08:00
def __init__(self, appid: str = None, appsecret: str = None):
self.appid = appid or settings.WECHAT_APPID
self.appsecret = appsecret or settings.WECHAT_APPSECRET
self._access_token = ""
self.timeout = 10.0 # 设置10秒超时
self.max_retries = 3 # 最大重试次数
2024-12-31 22:27:04 +08:00
async def _get(self, url: str) -> Dict[str, Any]:
"""带重试功能的GET请求"""
retry_count = 0
last_error = None
while retry_count < self.max_retries:
try:
logger.info(f"请求微信API: {url}")
async with httpx.AsyncClient(verify=False, timeout=self.timeout) as client:
response = await client.get(url)
logger.info(f"微信API响应: {response.status_code}")
return response.json()
except (httpx.ConnectTimeout, httpx.ReadTimeout) as e:
retry_count += 1
last_error = e
logger.warning(f"请求微信API超时{retry_count}次重试: {str(e)}")
if retry_count < self.max_retries:
# 等待一段时间再重试,使用指数退避策略
await asyncio.sleep(1 * (2 ** (retry_count - 1)))
# 所有重试都失败
logger.error(f"请求微信API失败已重试{self.max_retries}次: {str(last_error)}")
# 如果开发模式已启用,则返回模拟数据
if hasattr(settings, 'DEV_MODE') and settings.DEV_MODE:
# 返回模拟数据
logger.info("使用模拟数据替代微信API响应")
if 'jscode2session' in url:
return {
"openid": f"test_openid_{int(time.time())}",
"session_key": "test_session_key",
"unionid": f"test_unionid_{int(time.time())}"
}
elif 'token' in url:
return {"access_token": "test_access_token", "expires_in": 7200}
# 如果没有开发模式或者不是可以模拟的API则抛出异常
raise last_error
2024-12-31 22:27:04 +08:00
async def _post(self, url: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""带重试功能的POST请求"""
retry_count = 0
last_error = None
while retry_count < self.max_retries:
try:
logger.info(f"POST请求微信API: {url}")
async with httpx.AsyncClient(verify=False, timeout=self.timeout) as client:
response = await client.post(url, json=data)
logger.info(f"微信API响应: {response.status_code}")
return response.json()
except (httpx.ConnectTimeout, httpx.ReadTimeout) as e:
retry_count += 1
last_error = e
logger.warning(f"POST请求微信API超时{retry_count}次重试: {str(e)}")
if retry_count < self.max_retries:
await asyncio.sleep(1 * (2 ** (retry_count - 1)))
logger.error(f"POST请求微信API失败已重试{self.max_retries}次: {str(last_error)}")
raise last_error
2024-12-31 22:27:04 +08:00
async def get_access_token(self) -> str:
"""获取access token"""
url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.appid}&secret={self.appsecret}"
result = await self._get(url)
if "access_token" in result:
self._access_token = result["access_token"]
return self._access_token
raise Exception(f"Failed to get access token: {result}")
async def code2session(self, code: str) -> Dict[str, Any]:
"""小程序登录凭证校验"""
url = f"https://api.weixin.qq.com/sns/jscode2session?appid={self.appid}&secret={self.appsecret}&js_code={code}&grant_type=authorization_code"
result = await self._get(url)
if "errcode" in result and result["errcode"] != 0:
raise Exception(f"Code2Session failed: {result['errmsg']}code:{code}")
2024-12-31 22:27:04 +08:00
return result
async def get_unlimited_qrcode(self, scene: str, access_token: str = None) -> bytes:
"""获取小程序码"""
if not access_token:
access_token = await self.get_access_token()
url = f"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}"
data = {"scene": scene}
async with httpx.AsyncClient() as client:
response = await client.post(url, json=data, verify=False)
return response.content
async def send_template_message(self, data: Dict[str, Any], access_token: str = None) -> Dict[str, Any]:
"""发送订阅消息"""
if not access_token:
access_token = await self.get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token={access_token}"
return await self._post(url, data)
async def get_callback_ip(self, access_token: str = None) -> Dict[str, Any]:
"""获取微信服务器IP地址"""
if not access_token:
access_token = await self.get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token={access_token}"
return await self._get(url)
async def check_callback(self, access_token: str = None, action: str = "all") -> Dict[str, Any]:
"""检查回调配置"""
if not access_token:
access_token = await self.get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/callback/check?access_token={access_token}"
data = {"action": action, "check_operator": "DEFAULT"}
return await self._post(url, data)
async def create_menu(self, menu_data: Dict[str, Any], access_token: str = None) -> Dict[str, Any]:
"""创建自定义菜单"""
if not access_token:
access_token = await self.get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/menu/create?access_token={access_token}"
return await self._post(url, menu_data)
async def get_menu(self, access_token: str = None) -> Dict[str, Any]:
"""获取自定义菜单"""
if not access_token:
access_token = await self.get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/menu/get?access_token={access_token}"
return await self._get(url)
def get_oauth_url(self, redirect_uri: str, scope: str, state: str = "") -> str:
"""生成网页授权URL"""
url = (f"https://open.weixin.qq.com/connect/oauth2/authorize?"
f"appid={self.appid}&redirect_uri={redirect_uri}&response_type=code&"
f"scope={scope}#wechat_redirect")
if state:
url += f"&state={state}"
return url
async def get_user_token(self, code: str) -> Dict[str, Any]:
"""获取用户访问令牌"""
url = (f"https://api.weixin.qq.com/sns/oauth2/access_token?"
f"appid={self.appid}&secret={self.appsecret}&code={code}&"
f"grant_type=authorization_code")
result = await self._get(url)
if "access_token" not in result:
return {"code": 4001, "message": "获取用户授权失败"}
return result
async def refresh_token(self, refresh_token: str) -> Dict[str, Any]:
"""刷新访问令牌"""
url = (f"https://api.weixin.qq.com/sns/oauth2/refresh_token?"
f"appid={self.appid}&grant_type=refresh_token&refresh_token={refresh_token}")
return await self._get(url)
async def check_user_token(self, access_token: str, openid: str) -> bool:
"""检查用户访问令牌是否有效"""
url = f"https://api.weixin.qq.com/sns/auth?access_token={access_token}&openid={openid}"
result = await self._get(url)
return result.get("errcode", -1) == 0
async def get_user_info(self, access_token: str, openid: str,
refresh_token: str = "") -> Dict[str, Any]:
"""获取用户信息"""
if await self.check_user_token(access_token, openid):
url = (f"https://api.weixin.qq.com/sns/userinfo?"
f"access_token={access_token}&openid={openid}&lang=zh_CN")
return await self._get(url)
elif refresh_token:
result = await self.refresh_token(refresh_token)
new_token = result.get("access_token")
if new_token:
return await self.get_user_info(new_token, openid)
return {"code": 4002, "message": "获取用户详细信息失败请重新获取code"}
async def save_unlimited_qrcode(self, uid: Union[str, int],
path: str, filename: str,
access_token: str = None) -> bool:
"""生成并保存小程序码到文件"""
if not access_token:
access_token = await self.get_access_token()
url = f"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}"
data = {"scene": f"uid={uid}"}
retry_count = 0
last_error = None
while retry_count < self.max_retries:
try:
async with httpx.AsyncClient(verify=False, timeout=self.timeout) as client:
response = await client.post(url, json=data)
if response.status_code == 200:
# 确保目录存在
os.makedirs(os.path.dirname(f"{path}{filename}"), exist_ok=True)
full_path = f"{path}{filename}"
with open(full_path, 'wb') as f:
f.write(response.content)
return True
except Exception as e:
retry_count += 1
last_error = e
logger.warning(f"生成二维码失败,第{retry_count}次重试: {str(e)}")
if retry_count < self.max_retries:
await asyncio.sleep(1 * (2 ** (retry_count - 1)))
logger.error(f"生成二维码失败,已重试{self.max_retries}次: {str(last_error)}")
return False
wx_api = WeChatAPI()