Files

98 lines
3.8 KiB
Python
Raw Permalink 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.
"""模块:微信 access_token 管理
支持小程序(mini)与公众号(official)两种类型,基于 Django cache 缓存。
无 Redis 时仍可用(locmem 或内存降级,多进程下可能重复获取 token)。
配置(secrets.ini [wechat] 段):
APPID / SECRET 小程序或默认应用
OFFICIAL_APPID / OFFICIAL_SECRET 公众号(若与小程序不同,未配置则回退 APPID/SECRET)
常用 API:
from gvsds.wechat import AccessToken
token = AccessToken.Get('mini') # 带缓存
token = AccessToken.Get('official')
token = AccessToken.Refresh('mini') # 强制刷新
"""
import logging
from django.core.cache import cache
from gvsds.secrets import get as _secret_get
logger = logging.getLogger(__name__)
_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/token'
_CACHE_PREFIX = 'gvsds:wechat:token'
_CACHE_TTL = 7000 # 微信 token 7200s,留 200s 余量
_VALID_TYPES = ('mini', 'official')
class AccessToken:
"""微信 access_token 管理器。
通过 Get 获取(带缓存),通过 Refresh 强制刷新。
缓存 key 形如 gvsds:wechat:token:{type}:{appid}。
"""
@staticmethod
def _GetConfig(app_type):
"""读取对应类型的 AppID/Secret,official 未配置时回退默认。"""
if app_type == 'official':
appid = _secret_get('wechat', 'OFFICIAL_APPID') or _secret_get('wechat', 'APPID')
secret = _secret_get('wechat', 'OFFICIAL_SECRET') or _secret_get('wechat', 'SECRET')
else:
appid = _secret_get('wechat', 'APPID')
secret = _secret_get('wechat', 'SECRET')
if not appid or not secret:
raise RuntimeError(f'[AccessToken] 未配置 wechat {app_type} 的 APPID/SECRET')
return appid, secret
@staticmethod
def _CacheKey(app_type, appid):
return f'{_CACHE_PREFIX}:{app_type}:{appid}'
@classmethod
def Get(cls, app_type='mini'):
"""获取 access_token,优先走缓存;缓存命中失败时自动请求微信。"""
if app_type not in _VALID_TYPES:
raise RuntimeError(f'[AccessToken] 不支持的类型: {app_type}')
appid, _ = cls._GetConfig(app_type)
key = cls._CacheKey(app_type, appid)
try:
cached = cache.get(key)
if cached:
return cached
except Exception:
logger.warning('[AccessToken] 读取缓存失败,将直接请求微信', exc_info=True)
return cls.Refresh(app_type)
@classmethod
def Refresh(cls, app_type='mini'):
"""强制刷新 access_token 并写回缓存。失败时抛 RuntimeError。"""
if app_type not in _VALID_TYPES:
raise RuntimeError(f'[AccessToken] 不支持的类型: {app_type}')
appid, secret = cls._GetConfig(app_type)
params = {
'grant_type': 'client_credential',
'appid': appid,
'secret': secret,
}
try:
import requests
resp = requests.get(_TOKEN_URL, params=params, timeout=10)
data = resp.json()
except Exception as e:
logger.error('[AccessToken] 请求微信失败: %s', e, exc_info=True)
raise RuntimeError(f'[AccessToken] 请求微信失败: {e}')
if 'access_token' not in data:
errcode = data.get('errcode')
errmsg = data.get('errmsg', 'unknown')
logger.error('[AccessToken] 微信返回错误 errcode=%s errmsg=%s', errcode, errmsg)
raise RuntimeError(f'[AccessToken] 微信返回错误: {errcode} {errmsg}')
token = data['access_token']
try:
cache.set(cls._CacheKey(app_type, appid), token, _CACHE_TTL)
except Exception:
logger.warning('[AccessToken] 写入缓存失败', exc_info=True)
return token