# ============================================================================= # 企微IT智能服务台 — access_token 缓存管理器 # ============================================================================= # 说明:管理企微 access_token 的获取和缓存 # 1. 优先从 Redis 缓存获取 # 2. 缓存不存在或即将过期则重新获取 # 3. access_token 有效期 7200 秒,提前 300 秒刷新 # ============================================================================= import logging from typing import Optional import httpx import redis.asyncio as aioredis from app.config import settings logger = logging.getLogger(__name__) class TokenManager: """企微 access_token 缓存管理器。 封装 access_token 的获取、缓存和自动刷新逻辑。 使用 Redis 作为缓存存储,避免频繁调用企微 API。 Attributes: redis: Redis 异步客户端 corp_id: 企业ID corp_secret: 应用Secret """ # Redis 缓存 key CACHE_KEY = "wecom:access_token" # access_token 有效期(秒) TOKEN_EXPIRES = 7200 # 提前刷新时间(秒) BUFFER_SECONDS = 300 def __init__(self, redis_client: aioredis.Redis): """初始化 token 管理器。 Args: redis_client: Redis 异步客户端实例 """ self.redis = redis_client self.corp_id = settings.wecom_corp_id self.corp_secret = settings.wecom_secret self.client = httpx.AsyncClient(timeout=httpx.Timeout(connect=5.0, read=10.0)) async def get_token(self) -> str: """获取 access_token。 优先从 Redis 缓存获取,缓存未命中则调用企微 API 获取。 Returns: str: access_token 字符串 Raises: Exception: 获取失败 """ # 1. 尝试从缓存获取 cached = await self.redis.get(self.CACHE_KEY) if cached: logger.debug("从缓存获取 access_token") return cached.decode("utf-8") # 2. 缓存未命中,刷新 token return await self._refresh_token() async def _refresh_token(self) -> str: """调用企微 API 刷新 access_token。 对应企微API: GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET Returns: str: 新获取的 access_token Raises: Exception: 获取失败 """ logger.info("刷新 access_token") url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" params = { "corpid": self.corp_id, "corpsecret": self.corp_secret, } try: response = await self.client.get(url, params=params) result = response.json() if result.get("errcode") != 0: error_msg = result.get("errmsg", "未知错误") logger.error(f"获取 access_token 失败: {error_msg}") raise Exception(f"企微API错误: {error_msg}") access_token = result["access_token"] expires_in = result.get("expires_in", self.TOKEN_EXPIRES) # 缓存到 Redis,TTL = 有效期 - 提前刷新时间 cache_ttl = max(expires_in - self.BUFFER_SECONDS, 60) await self.redis.setex(self.CACHE_KEY, cache_ttl, access_token) logger.info(f"access_token 刷新成功,TTL={cache_ttl}秒") return access_token except httpx.HTTPError as e: logger.error(f"获取 access_token 网络错误: {e}") raise Exception(f"网络错误: {e}") from e async def invalidate(self) -> None: """手动使缓存失效。 当检测到 token 过期或无效时调用,强制下次请求刷新。 """ await self.redis.delete(self.CACHE_KEY) logger.info("access_token 缓存已手动清除") async def close(self) -> None: """关闭 HTTP 客户端。""" await self.client.aclose()