Program/mooc/utils/wechat_client.py
?..濡.. 8451ad034c 1.统一CRUD操作
2.完成登录部分接口
3.暂时挂载本地图片链接作为头像存储
2025-03-04 20:36:52 +08:00

224 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import httpx
from typing import Optional, Dict, Any, Union
from mooc.core.config import settings
import asyncio
import time
import logging
import os
logger = logging.getLogger(__name__)
class WeChatAPI:
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 # 最大重试次数
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
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
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}")
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()