225 lines
9.1 KiB
Python
225 lines
9.1 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
import asyncio
|
|
import httpx
|
|
import json
|
|
|
|
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
|
|
from app.utils.logger import get_logger
|
|
|
|
REDIS_KEY = "douyin:tokens"
|
|
logger = get_logger("token_refresh", "token_refresh")
|
|
|
|
|
|
|
|
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
|
|
from sqlalchemy import update
|
|
|
|
where_cond = UserOAuth.deleted_at.is_(None)
|
|
if oauth.appid:
|
|
where_cond = where_cond & (UserOAuth.appid == oauth.appid)
|
|
if oauth.account_username:
|
|
where_cond = where_cond & (UserOAuth.account_username == oauth.account_username)
|
|
if oauth.account_userid:
|
|
where_cond = where_cond & (UserOAuth.account_userid == oauth.account_userid)
|
|
|
|
await db.execute(
|
|
update(UserOAuth).where(where_cond).values(
|
|
access_token=None,
|
|
access_token_expired=None,
|
|
refresh_token=None,
|
|
refresh_token_expired=None,
|
|
)
|
|
)
|
|
await db.commit()
|
|
|
|
related_oauth_ids = await db.execute(
|
|
select(UserOAuth.id).where(where_cond)
|
|
)
|
|
related_oauth_ids = [row[0] for row in related_oauth_ids.all()]
|
|
for related_id in related_oauth_ids:
|
|
await _update_redis_token(related_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(timezone.utc) + timedelta(seconds=data.get("expires_in", 0))
|
|
refresh_token_expires_in = datetime.now(timezone.utc) + timedelta(seconds=data.get("refresh_token_expires_in", 0))
|
|
|
|
from sqlalchemy import update
|
|
|
|
where_cond = UserOAuth.deleted_at.is_(None)
|
|
if oauth.appid:
|
|
where_cond = where_cond & (UserOAuth.appid == oauth.appid)
|
|
if oauth.account_username:
|
|
where_cond = where_cond & (UserOAuth.account_username == oauth.account_username)
|
|
if oauth.account_userid:
|
|
where_cond = where_cond & (UserOAuth.account_userid == oauth.account_userid)
|
|
|
|
await db.execute(
|
|
update(UserOAuth).where(where_cond).values(
|
|
access_token=new_access_token,
|
|
access_token_expired=expires_in,
|
|
refresh_token=new_refresh_token,
|
|
refresh_token_expired=refresh_token_expires_in,
|
|
)
|
|
)
|
|
await db.commit()
|
|
|
|
related_oauth_ids = await db.execute(
|
|
select(UserOAuth.id).where(where_cond)
|
|
)
|
|
related_oauth_ids = [row[0] for row in related_oauth_ids.all()]
|
|
for related_id in related_oauth_ids:
|
|
await _update_redis_token(related_id, new_access_token, expires_in)
|
|
|
|
logger.info(f"成功刷新巨量引擎token: oauth_id={oauth.id}, account_id={oauth.account_id}, 关联账户数={len(related_oauth_ids)}")
|
|
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.refresh_token.is_not(None),
|
|
UserOAuth.refresh_token_expired.is_not(None),
|
|
UserOAuth.refresh_token_expired > now,
|
|
)
|
|
|
|
result = await db.execute(query)
|
|
oauth_list = result.scalars().all()
|
|
|
|
refreshed_keys = set()
|
|
|
|
for oauth in oauth_list:
|
|
try:
|
|
#检查是否为支持的平台(巨量引擎)
|
|
# port_type: 平台端口(1=巨量,2=磁力,3=巨量星图,4=服务单,5=腾讯)
|
|
if oauth.port_type not in [1]:
|
|
continue
|
|
|
|
#构建登录账号唯一标识,同一登录账号共享token
|
|
key_parts = []
|
|
if oauth.appid:
|
|
key_parts.append(oauth.appid)
|
|
if oauth.account_username:
|
|
key_parts.append(oauth.account_username)
|
|
if oauth.account_userid:
|
|
key_parts.append(oauth.account_userid)
|
|
login_key = "|".join(key_parts)
|
|
|
|
#同一登录账号已刷新过,直接跳过(避免使用旧数据判断)
|
|
if login_key in refreshed_keys:
|
|
logger.debug(f"跳过重复刷新: oauth_id={oauth.id}, 同一登录账号已刷新")
|
|
continue
|
|
|
|
#获取应用配置
|
|
app_result = await db.execute(
|
|
select(UserOAuthApp).where(UserOAuthApp.app_id == oauth.appid)
|
|
)
|
|
app = app_result.scalar_one_or_none()
|
|
|
|
if not app:
|
|
continue
|
|
|
|
#检查access_token是否需要刷新
|
|
need_refresh = False
|
|
|
|
# access_token为空,需要刷新
|
|
if not oauth.access_token:
|
|
need_refresh = True
|
|
# access_token_expired为空,需要刷新
|
|
elif not oauth.access_token_expired:
|
|
need_refresh = True
|
|
# access_token即将过期(剩余时间小于800秒),需要刷新
|
|
else:
|
|
remaining_seconds = (oauth.access_token_expired - now).total_seconds()
|
|
if remaining_seconds < REFRESH_THRESHOLD_SECONDS:
|
|
need_refresh = True
|
|
|
|
if not need_refresh:
|
|
continue
|
|
|
|
#refresh_token已在查询条件中过滤,确保有效才能刷新
|
|
|
|
#刷新token
|
|
await refresh_juliang_token(oauth, app, db)
|
|
|
|
refreshed_keys.add(login_key)
|
|
|
|
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()) |