新增统一时间工具,新增后台列表

This commit is contained in:
18610128193
2026-07-03 18:30:54 +08:00
parent a0016b436e
commit 3c0647a6aa
7 changed files with 1225 additions and 374 deletions
+87 -80
View File
@@ -1,49 +1,64 @@
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
import asyncio
import httpx
import json
from typing import Optional
from sqlalchemy import select
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
# 引入自定义北京时间工具类
from app.utils.datetime_util import BEIJING_TZ
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
from app.utils.datetime_util import datetime_to_db_tz_str, db_tz_str_to_datetime
REDIS_KEY = "douyin:tokens"
logger = get_logger("token_refresh", "token_refresh")
# 配置常量
REFRESH_THRESHOLD_SECONDS = 800
CHECK_INTERVAL_MINUTES = 5
HTTP_TIMEOUT = httpx.Timeout(30.0)
async def _delete_redis_token(oauth_id: str):
"""【改动1:废弃空值覆盖,直接删除Hash脏缓存】refresh失效则移除字段"""
redis = get_redis()
if not redis:
logger.warning("Redis连接未配置,跳过缓存清理")
return
try:
await redis.hdel(REDIS_KEY, oauth_id)
logger.info(f"清理失效授权Redis缓存: oauth_id={oauth_id}")
except Exception as e:
logger.error(f"删除Redis缓存失败 oauth_id:{oauth_id}, err:{str(e)}")
async def _update_redis_token(oauth_id: str, token: str, expired_at: datetime):
"""更新Redis缓存中的token"""
"""更新Redis缓存中的token(统一北京时间序列化)"""
redis = get_redis()
if not redis:
logger.warning("Redis连接未配置,跳过缓存更新")
return
try:
cache = {
"token": token,
"expired_at": expired_at.isoformat(),
}
"token": token,
"expired_at": datetime_to_db_tz_str(expired_at),
}
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)}")
logger.error(f"更新Redis缓存失败 oauth_id:{oauth_id}, err:{str(e)}")
async def refresh_juliang_token(oauth: UserOAuth, app: UserOAuthApp, db: AsyncSession):
"""刷新巨量引擎token"""
try:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
url = "https://api.oceanengine.com/open_api/oauth2/refresh_token/"
response = await client.post(
url,
@@ -55,13 +70,11 @@ async def refresh_juliang_token(oauth: UserOAuth, app: UserOAuthApp, db: AsyncSe
)
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
# refresh_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)
@@ -69,7 +82,7 @@ async def refresh_juliang_token(oauth: UserOAuth, app: UserOAuthApp, db: AsyncSe
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,
@@ -79,25 +92,23 @@ async def refresh_juliang_token(oauth: UserOAuth, app: UserOAuthApp, db: AsyncSe
)
)
await db.commit()
related_oauth_ids = await db.execute(
select(UserOAuth.id).where(where_cond)
)
related_oauth_ids = await db.execute(select(UserOAuth.id).where(where_cond))
related_oauth_ids = [row[0] for row in related_oauth_ids.all()]
# 【改动2:失效直接删除Redis缓存,不再写入空值】
for related_id in related_oauth_ids:
await _update_redis_token(related_id, "", None)
await _delete_redis_token(related_id)
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))
resp_data = data.get("data", {})
new_access_token = resp_data.get("access_token", "")
new_refresh_token = resp_data.get("refresh_token", "")
# 【改动3:全部使用北京时间计算过期时间,统一时区】
now_beijing = datetime.now(tz=BEIJING_TZ)
expires_in = now_beijing + timedelta(seconds=resp_data.get("expires_in", 0))
refresh_expires = now_beijing + timedelta(seconds=resp_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)
@@ -105,25 +116,26 @@ async def refresh_juliang_token(oauth: UserOAuth, app: UserOAuthApp, db: AsyncSe
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,
refresh_token_expired=refresh_expires,
)
)
await db.commit()
related_oauth_ids = await db.execute(
select(UserOAuth.id).where(where_cond)
)
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)}")
logger.info(
f"成功刷新巨量引擎token: oauth_id={oauth.id}, "
f"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:
@@ -133,93 +145,88 @@ async def refresh_juliang_token(oauth: UserOAuth, app: UserOAuthApp, db: AsyncSe
async def check_and_refresh_tokens():
"""检查并刷新即将过期的token"""
async with async_session() as db:
now = datetime.now(timezone.utc)
# 【改动4:统一使用北京时间当前时间,避免时区运算异常】
now_beijing = datetime.now(tz=BEIJING_TZ)
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,
UserOAuth.refresh_token_expired > now_beijing,
)
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)
key_parts.append(str(oauth.account_userid))
login_key = "|".join(key_parts)
#同一登录账号已刷新过,直接跳过(避免使用旧数据判断)
if login_key in refreshed_keys:
logger.debug(f"跳过重复刷新: oauth_id={oauth.id}, 同一登录账号已刷新")
continue
#获取应用配置
# 【改动5:过滤已禁用、已删除应用】
app_result = await db.execute(
select(UserOAuthApp).where(UserOAuthApp.app_id == oauth.appid)
select(UserOAuthApp).where(
UserOAuthApp.app_id == oauth.appid,
UserOAuthApp.status == 1,
UserOAuthApp.deleted_at.is_(None)
)
)
app = app_result.scalar_one_or_none()
if not app:
logger.warning(f"oauth_id:{oauth.id} 对应应用不存在/已禁用,跳过刷新")
continue
#检查access_token是否需要刷新
need_refresh = False
# access_token为空,需要刷新
if not oauth.access_token:
if not oauth.access_token or not oauth.access_token_expired:
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:
# 同时区时间运算,不会抛异常
remain_sec = (oauth.access_token_expired - now_beijing).total_seconds()
if remain_sec < 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)
# 简单限流,防止瞬间大量请求
await asyncio.sleep(0.2)
except Exception as e:
#7.增加错误日志
logger.error(f"刷新token失败: {str(e)}")
logger.error(f"oauth_id:{oauth.id} 刷新token异常: {str(e)}", exc_info=True)
async def token_refresh_scheduler():
"""定时任务调度器"""
logger.info("开始执行定时Token刷新任务")
while True:
try:
await check_and_refresh_tokens()
except Exception as e:
logger.error(f"定时任务token_refresh_scheduler执行失败: {str(e)}")
logger.error(f"定时任务token_refresh_scheduler执行失败: {str(e)}", exc_info=True)
await asyncio.sleep(CHECK_INTERVAL_MINUTES * 60)
def start_token_refresh_task():
"""启动token刷新定时任务"""
logger.info("启动token刷新定时任务")
asyncio.create_task(token_refresh_scheduler())
logger.info("启动token刷新定时后台任务")
asyncio.create_task(token_refresh_scheduler())