Files
video-gen/video-gen-api/app/tasks/token_refresh_task.py

194 lines
7.5 KiB
Python

from datetime import datetime, timedelta, timezone
import asyncio
import httpx
import json
import os
import logging
from logging.handlers import TimedRotatingFileHandler
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user_oauth import UserOAuth
from app.models.user_oauth_app import UserOAuthApp
from app.models.base import async_session
from app.config import settings
from app.utils.redis import get_redis
REDIS_KEY = "douyin:tokens"
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
os.makedirs(LOG_DIR, exist_ok=True)
logger = logging.getLogger("token_refresh")
logger.setLevel(logging.INFO)
def get_log_filename():
return os.path.join(LOG_DIR, f"token_refresh-{datetime.now().strftime('%Y-%m-%d')}.log")
class DailyRotatingFileHandler(logging.FileHandler):
def __init__(self, directory, encoding=None):
self.directory = directory
filename = get_log_filename()
super().__init__(filename, encoding=encoding)
def emit(self, record):
current_filename = get_log_filename()
if self.baseFilename != current_filename:
self.close()
self.baseFilename = current_filename
self.stream = self._open()
super().emit(record)
handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
logger.addHandler(handler)
REFRESH_THRESHOLD_SECONDS = 800
CHECK_INTERVAL_MINUTES = 5
async def _update_redis_token(oauth_id: str, token: str, expired_at: datetime):
"""更新Redis缓存中的token"""
redis = get_redis()
if not redis:
logger.warning("Redis连接未配置,跳过缓存更新")
return
try:
cache = {
"token": token,
"expired_at": expired_at.isoformat(),
}
await redis.hset(REDIS_KEY, oauth_id, json.dumps(cache))
logger.info(f"Redis缓存已更新: oauth_id={oauth_id}")
except Exception as e:
logger.error(f"更新Redis缓存失败: {str(e)}")
async def refresh_juliang_token(oauth: UserOAuth, app: UserOAuthApp, db: AsyncSession):
"""刷新巨量引擎token"""
try:
async with httpx.AsyncClient() as client:
url = "https://api.oceanengine.com/open_api/oauth2/refresh_token/"
response = await client.post(
url,
json={
"app_id": app.app_id,
"secret": app.secret,
"refresh_token": oauth.refresh_token,
},
)
response.raise_for_status()
data = response.json()
if data.get("code") != 0:
logger.error(f"刷新巨量引擎token失败: oauth_id={oauth.id}, 错误信息: {data}")
#如果code=40103或者40107,传入refresh_token已失效,失效原因一般是由于refresh_token已被使用,或授权账号重新授权并生成了新的Token
if data.get("code") in [40103, 40107]:
#清空数据库中的token信息,和Redis缓存中的token
oauth.access_token = None
oauth.access_token_expired = None
oauth.refresh_token = None
oauth.refresh_token_expired = None
await db.commit()
await _update_redis_token(oauth.id, "", None)
return
data = data.get("data", {})
new_access_token = data.get("access_token", "")
new_refresh_token = data.get("refresh_token", "")
expires_in = datetime.now(tz=oauth.access_token_expired.tzinfo) + timedelta(seconds=data.get("expires_in", 0))
refresh_token_expires_in = datetime.now(tz=oauth.refresh_token_expired.tzinfo) + timedelta(seconds=data.get("refresh_token_expires_in", 0))
oauth.access_token = new_access_token
oauth.access_token_expired = expires_in
oauth.refresh_token = new_refresh_token
oauth.refresh_token_expires_in = refresh_token_expires_in
await db.commit()
await _update_redis_token(oauth.id, new_access_token, expires_in)
logger.info(f"成功刷新巨量引擎token: oauth_id={oauth.id}, account_id={oauth.account_id}")
except httpx.HTTPError as e:
logger.error(f"HTTP请求失败: oauth_id={oauth.id}, 错误: {str(e)}")
except Exception as e:
logger.error(f"刷新巨量引擎token发生异常: oauth_id={oauth.id}, 错误: {str(e)}")
async def check_and_refresh_tokens():
"""检查并刷新即将过期的token"""
async with async_session() as db:
now = datetime.now(timezone.utc)
query = select(UserOAuth).where(
UserOAuth.deleted_at.is_(None),
UserOAuth.access_token.is_not(None),
UserOAuth.access_token_expired.is_not(None),
UserOAuth.refresh_token.is_not(None),
UserOAuth.refresh_token_expired.is_not(None),
)
result = await db.execute(query)
oauth_list = result.scalars().all()
for oauth in oauth_list:
try:
#1.检查access_token是否过期,如果未过期,并且大于800秒,直接跳过不处理
if not oauth.access_token_expired:
continue
remaining_seconds = (oauth.access_token_expired - now).total_seconds()
# access_token剩余时间大于等于800秒,不需要刷新
if remaining_seconds >= REFRESH_THRESHOLD_SECONDS:
continue
#2.如果access_token过期,或者剩余时间小于800秒,需要刷新token
#3.如果需要刷新token,检查refresh_token是否过期,如果refresh_token过期,说明不可刷新,需要直接重新授权,直接跳过不处理
if not oauth.refresh_token_expired:
continue
refresh_remaining_seconds = (oauth.refresh_token_expired - now).total_seconds()
if refresh_remaining_seconds <= 0:
continue
#5.获取应用配置
app_result = await db.execute(
select(UserOAuthApp).where(UserOAuthApp.app_id == oauth.appid)
)
app = app_result.scalar_one_or_none()
if not app:
continue
#检查是否为支持的平台(巨量引擎)
# port_type: 平台端口(1=巨量,2=磁力,3=巨量星图,4=服务单,5=腾讯)
if oauth.port_type in [1]:
#刷新token
await refresh_juliang_token(oauth, app, db)
except Exception as e:
#7.增加错误日志
logger.error(f"刷新token失败: {str(e)}")
async def token_refresh_scheduler():
"""定时任务调度器"""
while True:
try:
await check_and_refresh_tokens()
except Exception as e:
logger.error(f"定时任务token_refresh_scheduler执行失败: {str(e)}")
await asyncio.sleep(CHECK_INTERVAL_MINUTES * 60)
def start_token_refresh_task():
"""启动token刷新定时任务"""
logger.info("启动token刷新定时任务")
asyncio.create_task(token_refresh_scheduler())