新增更新token,提交素材,前测模板管理

This commit is contained in:
18610128193
2026-06-18 17:44:21 +08:00
parent c8ad49c736
commit 11f424a333
9 changed files with 648 additions and 62 deletions
+11 -11
View File
@@ -66,7 +66,7 @@ async def create_template(
db: AsyncSession = Depends(get_db),
) -> Any:
try:
#新增一个判断如果模板名称重复提示用户修改
#新增一个判断,如果模板名称重复,提示用户修改
result = await db.execute(
select(PreTestTemplate).where(
PreTestTemplate.name == req.name,
@@ -119,13 +119,13 @@ async def create_template(
@router.get(
"/list",
summary="获取前测模板列表",
description="获取当前用户的前测模板列表支持按平台筛选和分页",
description="获取当前用户的前测模板列表,支持按平台筛选和分页",
response_model=PreTestTemplateListResponse,
)
async def list_templates(
platform: Optional[str] = Query(None, description="投放平台筛选(AD/QIANCHUAN/LOCAL"),
page: int = Query(1, description="页码默认1"),
page_size: int = Query(10, description="每页数量默认10最大100"),
page: int = Query(1, description="页码,默认1"),
page_size: int = Query(10, description="每页数量,默认10,最大100"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
@@ -300,20 +300,20 @@ async def delete_template(
@router.get(
"/getArea",
summary="获取行政区域信息",
description="获取指定级别的行政区域信息支持一级、二级、三级区域,如果需要更新地区执行:/api/pre-test-template/getArea?oauth_id=0019ecab9b8bc57d964&advertiser_id=1836693172153543",
description="获取指定级别的行政区域信息,支持一级、二级、三级区域,如果需要更新地区,执行:/api/pre-test-template/getArea?oauth_id=0019ecab9b8bc57d964&advertiser_id=1836693172153543",
)
async def get_template_area(
oauth_id: str = Query(None, description="授权ID选填更新地区必填"),
advertiser_id: str = Query(default="1836693172153543", description="授权ID选填更新地区必填"),
code: Optional[str] = Query("CN", description="行政区域编码默认中国CN选填"),
level: Optional[str] = Query("ONE_LEVEL", description="行政区域层级可选值:ONE_LEVEL(获取省份)、TWO_LEVEL(市级)、THREE_LEVEL(区级)"),
parent_code: Optional[str] = Query(None, description="父级区域编码获取二级时传一级编码获取三级时传二级编码"),
oauth_id: str = Query(None, description="授权ID选填,更新地区必填"),
advertiser_id: str = Query(default="1836693172153543", description="授权ID选填,更新地区必填"),
code: Optional[str] = Query("CN", description="行政区域编码,默认中国CN,选填"),
level: Optional[str] = Query("ONE_LEVEL", description="行政区域层级,可选值:ONE_LEVEL(获取省份)、TWO_LEVEL(市级)、THREE_LEVEL(区级)"),
parent_code: Optional[str] = Query(None, description="父级区域编码,获取二级时传一级编码,获取三级时传二级编码"),
) -> Any:
try:
# 1. 先检查缓存是否存在
area_list = get_cached_area_data()
# 2. 如果缓存不存在调用接口获取数据并保存到缓存
# 2. 如果缓存不存在,调用接口获取数据并保存到缓存
if not area_list:
area_list = await fetch_and_cache_area_data(oauth_id, advertiser_id, code)
+122 -15
View File
@@ -1,12 +1,16 @@
import os
from typing import Any, Optional
from typing import Any, Optional, Dict
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_user, get_db
from app.models.user import User
from app.models.pre_test_template import PreTestTemplate
from app.services.upload_material_service import upload_material_to_platform
from sqlalchemy import select
from app.utils.douyinApi import DouyinApi
router = APIRouter(prefix="/upload-material", tags=["上传素材"])
@@ -15,8 +19,8 @@ class UploadTask(BaseModel):
advertiser_ids: list[str] = Field(..., description="广告主id数组,支持多条")
resource_ids: list[str] = Field(..., description="资源id数组(generated_resources表主键)")
oauth_id: str = Field(..., description="授权表id")
is_pre_test: Optional[str] = Field(None, description="是否开启前测:是/否/0/1,预留字段")
pre_test_template: Optional[str] = Field(None, description="前测模板id,预留字段")
is_pre_test: Optional[str] = Field(None, description="是否开启前测:1=是/2=否")
pre_test_template: Optional[str] = Field(None, description="前测模板id")
class BatchUploadRequest(BaseModel):
@@ -33,11 +37,35 @@ async def batch_upload_material(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
# try:
# params = {
# "advertiser_id": 1836693172153543,
# "video_ids": ["tos-cn-i-sd07hgqsbj/2db9a53ee3ff4b6f8a2763b3bb463047"],
# "diagnose_config": {"platform": "AD", "external_action": "AD_APP_ACTIVATE"},
# }
# response = await DouyinApi().pre_test_material("0019ecab9b8bc57d964", params)
# return response
# except Exception as e:
# import traceback
# return {
# "code": 500,
# "message": "请求失败",
# "error": str(e),
# "traceback": traceback.format_exc()
# }
if not req.tasks:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="上传任务列表不能为空",
)
return {
"code": 0,
"message": "批量上传完成",
"summary": {
"total_tasks": 0,
"total_success_count": 0,
"total_fail_count": 0,
"total_requested": 0,
},
"details": [],
"error": "上传任务列表不能为空"
}
all_results = []
total_success = 0
@@ -56,9 +84,73 @@ async def batch_upload_material(
try:
if not task.advertiser_ids:
raise ValueError("广告主id数组不能为空")
task_result["result"] = {
"success": False,
"error": "广告主id数组不能为空",
"success_count": 0,
"fail_count": 0,
"total_count": 0,
"results": [],
}
all_results.append(task_result)
continue
if not task.resource_ids:
raise ValueError("资源id数组不能为空")
task_result["result"] = {
"success": False,
"error": "资源id数组不能为空",
"success_count": 0,
"fail_count": 0,
"total_count": 0,
"results": [],
}
all_results.append(task_result)
continue
if (task.is_pre_test == "1") and (not task.pre_test_template):
task_result["result"] = {
"success": False,
"error": "开启前测功能时,必须指定前测模板id",
"success_count": 0,
"fail_count": len(task.resource_ids) * len(task.advertiser_ids),
"total_count": len(task.resource_ids) * len(task.advertiser_ids),
"results": [{
"resource_id": rid,
"advertiser_id": aid,
"filename": "",
"success": False,
"error": "开启前测功能时,必须指定前测模板id"
} for rid in task.resource_ids for aid in task.advertiser_ids],
}
total_fail += len(task.resource_ids) * len(task.advertiser_ids)
all_results.append(task_result)
continue
if task.is_pre_test == "1":
template = await db.execute(
select(PreTestTemplate).where(PreTestTemplate.id == task.pre_test_template).
where(PreTestTemplate.deleted_at.is_(None)).
where(PreTestTemplate.user_id == current_user.id)
)
template = template.scalar_one_or_none()
if not template:
task_result["result"] = {
"success": False,
"error": "前测模板id不存在",
"success_count": 0,
"fail_count": len(task.resource_ids) * len(task.advertiser_ids),
"total_count": len(task.resource_ids) * len(task.advertiser_ids),
"results": [{
"resource_id": rid,
"advertiser_id": aid,
"filename": "",
"success": False,
"error": "前测模板id不存在"
} for rid in task.resource_ids for aid in task.advertiser_ids],
}
total_fail += len(task.resource_ids) * len(task.advertiser_ids)
all_results.append(task_result)
continue
result = await upload_material_to_platform(
task.resource_ids,
@@ -66,6 +158,7 @@ async def batch_upload_material(
task.oauth_id,
db,
current_user.id,
task.pre_test_template if task.is_pre_test == "1" else None,
)
task_result["result"] = {
@@ -75,23 +168,37 @@ async def batch_upload_material(
total_success += result["success_count"]
total_fail += result["fail_count"]
if task.is_pre_test and task.is_pre_test in ["", "1", "true", True]:
task_result["pre_test_reserved"] = {
"status": "reserved",
"message": "前测功能已预留,待后续开通",
"template": task.pre_test_template,
}
except ValueError as e:
task_result["result"] = {
"success": False,
"error": str(e),
"success_count": 0,
"fail_count": len(task.resource_ids) * len(task.advertiser_ids),
"total_count": len(task.resource_ids) * len(task.advertiser_ids),
"results": [{
"resource_id": rid,
"advertiser_id": aid,
"filename": "",
"success": False,
"error": str(e)
} for rid in task.resource_ids for aid in task.advertiser_ids],
}
total_fail += len(task.resource_ids) * len(task.advertiser_ids)
except Exception as e:
task_result["result"] = {
"success": False,
"error": f"上传失败: {str(e)}",
"success_count": 0,
"fail_count": len(task.resource_ids) * len(task.advertiser_ids),
"total_count": len(task.resource_ids) * len(task.advertiser_ids),
"results": [{
"resource_id": rid,
"advertiser_id": aid,
"filename": "",
"success": False,
"error": f"上传失败: {str(e)}"
} for rid in task.resource_ids for aid in task.advertiser_ids],
}
total_fail += len(task.resource_ids) * len(task.advertiser_ids)
+2 -1
View File
@@ -86,7 +86,7 @@ async def juliang_callback(
await get_token(auth_code, user_id, app_id, db)
return {
"message": "授权成功",
"message": "授权成功,这里需要跳转页面路径到 /user-oauth/oauth_list",
"code": 0,
}
@@ -139,6 +139,7 @@ async def oauth_list(
"account_name": oauth.account_name,
"account_role": oauth.account_role,
"account_username": oauth.account_username,
"account_userid": oauth.account_userid,
"user_id": oauth.user_id,
"open_type": oauth.open_type,
"port_type": oauth.port_type,
+5
View File
@@ -59,6 +59,10 @@ async def lifespan(app: FastAPI):
expiry_task = asyncio.create_task(_order_expiry_loop())
# 启动token刷新定时任务(每5分钟检查一次,小于800秒有效期的token进行刷新)
from app.tasks.token_refresh_task import token_refresh_scheduler
token_refresh_task = asyncio.create_task(token_refresh_scheduler())
# 启动时立即同步一次未支付订单
asyncio.create_task(asyncio.sleep(5)) # 等待5秒后再同步,让系统完全启动
async def startup_sync():
@@ -82,6 +86,7 @@ async def lifespan(app: FastAPI):
task_queue.stop()
await queue_task
expiry_task.cancel()
token_refresh_task.cancel()
await close_database()
await close_redis()
@@ -1,12 +1,14 @@
import os
import hashlib
import base64
import json
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.generated_resource import GeneratedResource
from app.models.user_oauth import UserOAuth
from app.models.resources_material import ResourcesMaterial
from app.models.pre_test_template import PreTestTemplate
from app.utils.id_gen import generate_id
from app.utils.douyinApi import DouyinApi
@@ -20,6 +22,7 @@ async def upload_material_to_platform(
oauth_id: str,
db: AsyncSession,
current_user_id: str,
pre_test_template_id: str | None = None,
) -> any:
oauth = await db.execute(
select(UserOAuth).where(
@@ -30,13 +33,37 @@ async def upload_material_to_platform(
)
oauth = oauth.scalar_one_or_none()
if not oauth:
raise ValueError("授权记录不存在")
return {
"success_count": 0,
"fail_count": len(resource_ids) * len(advertiser_ids),
"total_count": len(resource_ids) * len(advertiser_ids),
"results": [{
"resource_id": rid,
"advertiser_id": aid,
"filename": "",
"success": False,
"error": "授权记录不存在"
} for rid in resource_ids for aid in advertiser_ids],
"error": "授权记录不存在"
}
port_type = oauth.port_type
access_token = oauth.access_token
if not access_token:
raise ValueError("授权token不存在")
return {
"success_count": 0,
"fail_count": len(resource_ids) * len(advertiser_ids),
"total_count": len(resource_ids) * len(advertiser_ids),
"results": [{
"resource_id": rid,
"advertiser_id": aid,
"filename": "",
"success": False,
"error": "授权token不存在"
} for rid in resource_ids for aid in advertiser_ids],
"error": "授权token不存在"
}
resources = await db.execute(
select(GeneratedResource).where(
@@ -48,26 +75,57 @@ async def upload_material_to_platform(
resources = resources.scalars().all()
resource_dict = {r.id: r for r in resources}
for resource_id in resource_ids:
if resource_id not in resource_dict:
raise ValueError(f"资源 {resource_id} 不存在或不属于当前用户")
all_upload_results = []
success_count = 0
fail_count = 0
for resource_id in resource_ids:
resource = resource_dict[resource_id]
resource_type = resource.resource_type
storage_path = resource.storage_path
if resource_id not in resource_dict:
for advertiser_id in advertiser_ids:
all_upload_results.append({
"resource_id": resource_id,
"advertiser_id": advertiser_id,
"filename": "",
"success": False,
"error": f"资源 {resource_id} 不存在或不属于当前用户"
})
fail_count += 1
if resource_type not in ["image", "video"]:
raise ValueError(f"资源 {resource_id} 不支持的资源类型,仅支持image和video")
for advertiser_id in advertiser_ids:
# 为每个广告主上传素材
for resource_id in resource_ids:
if resource_id not in resource_dict:
continue
if not storage_path:
raise ValueError(f"资源 {resource_id} 本地存储路径为空")
resource = resource_dict[resource_id]
resource_type = resource.resource_type
storage_path = resource.storage_path
if resource_type not in ["image", "video"]:
result = {
"resource_id": resource_id,
"advertiser_id": advertiser_id,
"filename": os.path.basename(storage_path) if storage_path else "",
"success": False,
"error": f"不支持的资源类型: {resource_type},仅支持image和video",
}
all_upload_results.append(result)
fail_count += 1
continue
if not storage_path:
result = {
"resource_id": resource_id,
"advertiser_id": advertiser_id,
"filename": "",
"success": False,
"error": "资源本地存储路径为空",
}
all_upload_results.append(result)
fail_count += 1
continue
for advertiser_id in advertiser_ids:
result = await _upload_to_juliang(
oauth_id, storage_path, resource_type, advertiser_id, resource, db, current_user_id
)
@@ -77,7 +135,17 @@ async def upload_material_to_platform(
else:
fail_count += 1
await db.commit()
# 上传完成后提交事务
await db.commit()
# 调用前测接口(仅当有模板ID且有视频上传成功时)
if pre_test_template_id:
success_video_ids = [
r["upload_id"] for r in all_upload_results
if r.get("success") and r.get("upload_id") and r.get("advertiser_id") == advertiser_id
]
if success_video_ids:
await _pre_test_material(oauth_id, advertiser_id, success_video_ids, pre_test_template_id, db)
return {
"success_count": success_count,
@@ -87,6 +155,159 @@ async def upload_material_to_platform(
}
#调用抖音前测接口
async def _pre_test_material(
oauth_id: str,
advertiser_id: str,
video_ids: list[str],
pre_test_template_id: str,
db: AsyncSession,
) -> any:
#1.查询数据库是否存在前测模板
pre_test_template = await db.execute(
select(PreTestTemplate).where(
PreTestTemplate.id == pre_test_template_id,
)
)
pre_test_template = pre_test_template.scalar_one_or_none()
if not pre_test_template:
note = f"前测模板 {pre_test_template_id} 不存在"
for video_id in video_ids:
await _update_material_pre_test_status(
db, oauth_id, advertiser_id, video_id,
task_id=None,
status="FAILED",
note=note,
pre_result=None,
)
await db.commit()
return {"code": -1, "message": note, "data": {}}
diagnose_config = {}
if pre_test_template.platform:
diagnose_config["platform"] = pre_test_template.platform
if pre_test_template.external_action:
diagnose_config["external_action"] = pre_test_template.external_action
if pre_test_template.cpa_bid:
diagnose_config["cpa_bid"] = pre_test_template.cpa_bid
if pre_test_template.audience_gender:
diagnose_config["audience_gender"] = pre_test_template.audience_gender
if pre_test_template.audience_age:
diagnose_config["audience_age"] = json.loads(pre_test_template.audience_age)
if pre_test_template.audience_region:
diagnose_config["audience_region"] = json.loads(pre_test_template.audience_region)
if pre_test_template.audience_network:
diagnose_config["audience_network"] = json.loads(pre_test_template.audience_network)
if pre_test_template.cus_name:
diagnose_config["cus_name"] = pre_test_template.cus_name
if pre_test_template.pricing_type:
diagnose_config["pricing_type"] = pre_test_template.pricing_type
if pre_test_template.cost_cap:
diagnose_config["cost_cap"] = pre_test_template.cost_cap
if pre_test_template.target_cost:
diagnose_config["target_cost"] = pre_test_template.target_cost
if pre_test_template.nobid:
diagnose_config["nobid"] = pre_test_template.nobid
if pre_test_template.cpc_bid:
diagnose_config["cpc_bid"] = pre_test_template.cpc_bid
if pre_test_template.budget:
diagnose_config["budget"] = pre_test_template.budget
params = {
"advertiser_id": int(advertiser_id),
"video_ids": video_ids,
"diagnose_config": diagnose_config,
}
response = await douyin_api.pre_test_material(oauth_id, params)
# 2. 解析接口返回并更新数据库
code = response.get("code", -1)
if code != 0:
# 整体失败:更新所有视频状态为FAILED
note = response.get("message", "未知错误")
for video_id in video_ids:
await _update_material_pre_test_status(
db, oauth_id, advertiser_id, video_id,
task_id=None,
status="FAILED",
note=note,
pre_result=None,
)
await db.commit()
return response
# 成功:解析 data 中的任务信息
data = response.get("data", {})
task_ids = data.get("task_ids", [])
fail_video_ids = data.get("fail_video_ids", {})
# 3. 更新成功的视频状态为PENDING(等待审核)
success_count = 0
for i, video_id in enumerate(video_ids):
if video_id in fail_video_ids:
# 失败的视频
fail_info = fail_video_ids[video_id]
err_code = fail_info.get("err_code", "")
err_message = fail_info.get("err_message", "未知错误")
note = f"失败[{err_code}]: {err_message}"
await _update_material_pre_test_status(
db, oauth_id, advertiser_id, video_id,
task_id=None,
status="FAILED",
note=note,
pre_result=None,
)
else:
# 成功的视频
task_id = str(task_ids[success_count]) if success_count < len(task_ids) else None
await _update_material_pre_test_status(
db, oauth_id, advertiser_id, video_id,
task_id=task_id,
status="PENDING",
note="",
pre_result=None,
)
success_count += 1
# 提交事务,保存所有修改
await db.commit()
return response
async def _update_material_pre_test_status(
db: AsyncSession,
oauth_id: str,
advertiser_id: str,
upload_id: str,
task_id: str | None,
status: str,
note: str | None,
pre_result: str | None,
):
"""更新素材的前测状态"""
result = await db.execute(
select(ResourcesMaterial).where(
ResourcesMaterial.oauth_id == oauth_id,
ResourcesMaterial.advertiser_id == advertiser_id,
ResourcesMaterial.upload_id == upload_id,
ResourcesMaterial.deleted_at.is_(None),
)
)
material = result.scalar_one_or_none()
if material:
material.task_id = task_id
material.status = status
material.note = note
material.pre_result = pre_result
async def _upload_to_juliang(
oauth_id: str,
storage_path: str,
@@ -0,0 +1,194 @@
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())
+59 -14
View File
@@ -1,5 +1,3 @@
from asyncio import Condition
import httpx
from datetime import datetime, timedelta
@@ -7,9 +5,13 @@ from sqlalchemy import select
from app.models.base import async_session
from app.models.user_oauth import UserOAuth
from app.models.user_oauth_account import UserOAuthAccount
from app.models.user_oauth_app import UserOAuthApp
from app.tasks.async_runner import run_async
from app.tasks.celery_app import celery_app
from app.utils.douyinApi import DouyinApi
from app.utils.douyinRequest import DouyinRequest
from app.utils.id_gen import generate_id
async def _update_oauth_accounts(account_id: str, account_userid: str, current_user_id: str, db: async_session):
@@ -27,18 +29,61 @@ async def _update_oauth_accounts(account_id: str, account_userid: str, current_u
if not oauth_records:
return True
for oauth in oauth_records:
pass
if oauth.port_type == 1:
return await update_juliang(oauth, db)
if celery_app:
@celery_app.task(name="user_oauth.update_oauth_accounts", bind=True, max_retries=3, default_retry_delay=60)
def update_oauth_accounts(self, account_id: str, account_userid: str, current_user_id: str, db):
return run_async(_update_oauth_accounts(account_id, account_userid, current_user_id, db))
else:
class _DisabledTask:
def delay(self, *args, **kwargs):
pass
async def update_juliang(oauth: UserOAuth, db: async_session):
if oauth.account_role == 'AGENT':
#通过代理商获取账户列表
cursor : int = 0
count : int = 10
while True:
params = {
'advertiser_id': oauth.account_id,
'count': count,
}
if cursor:
params['cursor'] = cursor
response = await DouyinApi().get_advertiser_by_agent(oauth.id, params)
if response.get('code', 0) != 0:
#记录错误日志
break
def apply_async(self, *args, **kwargs):
pass
data = response['data']['list'] or []
if not data:
break
account_source = response['data']['account_source'] or ''
update_oauth_accounts = _DisabledTask()
account_list = []
for item in data:
account_list.append(UserOAuthAccount(
id=generate_id(),
oauth_id=oauth.id,
advertiser_id=str(item),
advertiser_name="",
advertiser_role=account_source,
))
if account_list:
db.add_all(account_list)
await db.commit()
cursor = response['data'].get('cursor_page_info', {}).get('cursor')
has_more = response['data'].get('cursor_page_info', {}).get('has_more', False)
if not has_more:
break
# if celery_app:
# @celery_app.task(name="user_oauth.update_oauth_accounts", bind=True, max_retries=3, default_retry_delay=60)
# def update_oauth_accounts(self, account_id: str, account_userid: str, current_user_id: str, db):
# return run_async(_update_oauth_accounts(account_id, account_userid, current_user_id, db))
# else:
# class _DisabledTask:
# def delay(self, *args, **kwargs):
# pass
# def apply_async(self, *args, **kwargs):
# pass
# update_oauth_accounts = _DisabledTask()
+13
View File
@@ -37,6 +37,7 @@ class DouyinApi:
options
)
#上传视频素材
async def upload_video_material(self, oauth_id: str, data: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
if not oauth_id:
raise RuntimeError('OAuth ID is not set.')
@@ -66,3 +67,15 @@ class DouyinApi:
'GET',
{'params': params or {}}
)
#前测素材,Adv创建前测任务
async def pre_test_material(self, oauth_id: str, params: any) -> Dict[str, Any]:
if not oauth_id:
raise RuntimeError('OAuth ID is not set.')
url = "https://api.oceanengine.com/open_api/2/diagnosis_task/adv/create/"
return await self.request.request_with_token_with_context(
oauth_id,
url,
'POST',
{"json": params or {}}
)
+2 -2
View File
@@ -186,7 +186,7 @@ class DouyinRequest:
oauth_id: str,
url: str,
method: str = 'GET',
options: Optional[Dict[str, Any]] = None,
options: any = None,
) -> Any:
options = options or {}
token = await self.get_access_token(oauth_id)
@@ -229,7 +229,7 @@ class DouyinRequest:
await asyncio.sleep(wait_time)
continue
if code >= 50000:
if code == 50000:
await asyncio.sleep(i * 10)
continue