diff --git a/video-gen-api/app/api/v1/__init__.py b/video-gen-api/app/api/v1/__init__.py index a46e5c52..6bc09321 100644 --- a/video-gen-api/app/api/v1/__init__.py +++ b/video-gen-api/app/api/v1/__init__.py @@ -20,6 +20,7 @@ from app.api.v1.shot_replicate import router as shot_replicate_router from app.api.v1.test import router as test_router from app.api.v1.user_oauth import router as user_oauth_router from app.api.v1.user_oauth_app import router as user_oauth_app_router +from app.api.v1.upload_material import router as upload_material_router api_router = APIRouter() api_router.include_router(auth_router) @@ -42,3 +43,4 @@ api_router.include_router(shot_replicate_router) api_router.include_router(test_router) api_router.include_router(user_oauth_router) api_router.include_router(user_oauth_app_router) +api_router.include_router(upload_material_router) diff --git a/video-gen-api/app/api/v1/upload_material.py b/video-gen-api/app/api/v1/upload_material.py new file mode 100644 index 00000000..d3580019 --- /dev/null +++ b/video-gen-api/app/api/v1/upload_material.py @@ -0,0 +1,110 @@ +import os +from typing import Any, Optional +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.services.upload_material_service import upload_material_to_platform + +router = APIRouter(prefix="/upload-material", tags=["上传素材"]) + + +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,预留字段") + + +class BatchUploadRequest(BaseModel): + tasks: list[UploadTask] = Field(..., description="批量上传任务列表") + + +@router.post( + "/batch-upload", + summary="批量上传素材到平台", + description="支持批量上传多个授权账户下的资源到素材库,预留下前测功能", +) +async def batch_upload_material( + req: BatchUploadRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + if not req.tasks: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="上传任务列表不能为空", + ) + + all_results = [] + total_success = 0 + total_fail = 0 + + for task_index, task in enumerate(req.tasks, 1): + task_result = { + "task_index": task_index, + "oauth_id": task.oauth_id, + "advertiser_ids": task.advertiser_ids, + "resource_ids": task.resource_ids, + "is_pre_test": task.is_pre_test, + "pre_test_template": task.pre_test_template, + "result": None, + } + + try: + if not task.advertiser_ids: + raise ValueError("广告主id数组不能为空") + if not task.resource_ids: + raise ValueError("资源id数组不能为空") + + result = await upload_material_to_platform( + task.resource_ids, + task.advertiser_ids, + task.oauth_id, + db, + current_user.id, + ) + + task_result["result"] = { + "success": True, + **result, + } + 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), + } + total_fail += len(task.resource_ids) * len(task.advertiser_ids) + except Exception as e: + task_result["result"] = { + "success": False, + "error": f"上传失败: {str(e)}", + } + total_fail += len(task.resource_ids) * len(task.advertiser_ids) + + all_results.append(task_result) + + return { + "code": 0, + "message": "批量上传完成", + "summary": { + "total_tasks": len(req.tasks), + "total_success_count": total_success, + "total_fail_count": total_fail, + "total_requested": sum(len(t.resource_ids) * len(t.advertiser_ids) for t in req.tasks), + }, + "details": all_results, + } \ No newline at end of file diff --git a/video-gen-api/app/api/v1/user_oauth.py b/video-gen-api/app/api/v1/user_oauth.py index 89149339..72aa50f0 100644 --- a/video-gen-api/app/api/v1/user_oauth.py +++ b/video-gen-api/app/api/v1/user_oauth.py @@ -10,8 +10,9 @@ from app.schemas.user_oauth import RequestOAuthRequest, RequestOAuthResponse, Us from app.services.user_oauth_service import ( build_oauth_url, get_token, + get_oauth_list, ) -from app.tasks.user_oauth_tasks import update_oauth_accounts +from app.tasks.user_oauth_tasks import _update_oauth_accounts router = APIRouter(prefix="/user-oauth", tags=["oauth"]) @@ -28,15 +29,17 @@ async def request_oauth( db: AsyncSession = Depends(get_db), ): try: + if req.open_type not in [1,2,3,4,5,6,7,8,9,10]: + raise ValueError("open_type must be in [1,2,3,4,5,6,7,8,9,10]") + auth_url = await build_oauth_url(req.open_type, current_user.id, db) return {"auth_url": auth_url} except ValueError as e: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_404_NOT_FOUND, detail=str(e), ) - @router.get( "/juliang_callback", summary="巨量授权回调", @@ -102,64 +105,62 @@ async def juliang_callback( ) @router.get( - "/update_account", - summary="更新权限下的所有账户", - description="用户提交授权登录账户id,或者授权id", + "/oauth_list", + summary="获取账户下所有授权列表", + description="获取当前用户下所有授权账户列表,支持按授权登录账号、开户方式、授权账户id筛选", ) -async def update_account( - account_id: str | None = Query(None, description="授权账户id"), +async def oauth_list( account_userid: str | None = Query(None, description="授权登录账号id"), + open_type: int | None = Query(None, description="开户方式open_type"), + account_id: str | None = Query(None, description="授权账户id"), + page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): try: - if not account_id and not account_userid: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="请提交授权账户id或授权登录账号id", - ) - #获取单独授权账号 - if account_id: - exist = await db.execute( - select(UserOAuth).where( - UserOAuth.account_id == account_id, - UserOAuth.user_id == current_user.id, - UserOAuth.deleted_at.is_(None), - ).limit(1) - ) - exist = exist.scalar_one_or_none() - if not exist: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="授权账户不存在", - ) - #获取授权登录账号的所有账号 - if account_userid: - exist = await db.execute( - select(UserOAuth).where( - UserOAuth.account_userid == account_userid, - UserOAuth.user_id == current_user.id, - UserOAuth.deleted_at.is_(None), - ).limit(1) - ) - exist = exist.scalar_one_or_none() - if not exist: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="授权登录账号不存在", - ) - await update_oauth_accounts(account_id, account_userid, current_user.id, db) - return {"message": "提交成功,等待处理", "code": 0} + result = await get_oauth_list( + user_id=current_user.id, + db=db, + account_userid=account_userid, + open_type=open_type, + account_id=account_id, + page=page, + page_size=page_size, + ) + + return { + "code": 0, + "message": "查询成功", + "data": [ + { + "id": oauth.id, + "account_id": oauth.account_id, + "account_name": oauth.account_name, + "account_role": oauth.account_role, + "account_username": oauth.account_username, + "user_id": oauth.user_id, + "open_type": oauth.open_type, + "port_type": oauth.port_type, + "appid": oauth.appid, + "material_auth_status": oauth.material_auth_status, + "created_at": oauth.created_at, + "updated_at": oauth.updated_at, + } + for oauth in result["data"] + ], + "pagination": { + "page": result["page"], + "page_size": result["page_size"], + "total": result["total"], + "total_pages": (result["total"] + result["page_size"] - 1) // result["page_size"], + }, + } except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), ) - except HTTPException as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), - ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/video-gen-api/app/models/user_oauth_account.py b/video-gen-api/app/models/user_oauth_account.py index 35a01240..dd9afe85 100644 --- a/video-gen-api/app/models/user_oauth_account.py +++ b/video-gen-api/app/models/user_oauth_account.py @@ -10,12 +10,12 @@ class UserOAuthAccount(Base, TimestampMixin, SoftDeleteMixin): id: Mapped[str] = mapped_column( String(32), primary_key=True, comment="主键" ) - account_id: Mapped[str] = mapped_column( + oauth_id: Mapped[str] = mapped_column( String(64), - nullable=False, index=True, comment="授权账户id(user_oauth表中同一个)" + nullable=False, index=True, comment="授权表中的id" ) advertiser_id: Mapped[str | None] = mapped_column( - String(64), nullable=True, index=True, comment="广告账户id" + String(64), nullable=True, index=True, comment="广告主账户id" ) advertiser_name: Mapped[str | None] = mapped_column( String(128), nullable=True, comment="广告账户名" diff --git a/video-gen-api/app/models/user_oauth_app.py b/video-gen-api/app/models/user_oauth_app.py index 377a6361..0a31eed6 100644 --- a/video-gen-api/app/models/user_oauth_app.py +++ b/video-gen-api/app/models/user_oauth_app.py @@ -19,7 +19,7 @@ class UserOAuthApp(Base, TimestampMixin, SoftDeleteMixin): status: Mapped[int] = mapped_column( BigInteger, nullable=False, default=1, comment="状态,1=正常,2=禁用" ) - count: Mapped[int] = mapped_column( + max_count: Mapped[int] = mapped_column( BigInteger, nullable=False, default=100, comment="应用最大可以授权多少个用户" ) auth_url: Mapped[str] = mapped_column( diff --git a/video-gen-api/app/services/upload_material_service.py b/video-gen-api/app/services/upload_material_service.py new file mode 100644 index 00000000..df6a8a2d --- /dev/null +++ b/video-gen-api/app/services/upload_material_service.py @@ -0,0 +1,310 @@ +import os +import hashlib +import base64 +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.utils.id_gen import generate_id +from app.utils.douyinApi import DouyinApi + + +douyin_api = DouyinApi() + + +async def upload_material_to_platform( + resource_ids: list[str], + advertiser_ids: list[str], + oauth_id: str, + db: AsyncSession, + current_user_id: str, +) -> any: + oauth = await db.execute( + select(UserOAuth).where( + UserOAuth.id == oauth_id, + UserOAuth.deleted_at.is_(None), + UserOAuth.user_id == current_user_id, + ) + ) + oauth = oauth.scalar_one_or_none() + if not oauth: + raise ValueError("授权记录不存在") + + port_type = oauth.port_type + access_token = oauth.access_token + + if not access_token: + raise ValueError("授权token不存在") + + resources = await db.execute( + select(GeneratedResource).where( + GeneratedResource.id.in_(resource_ids), + GeneratedResource.user_id == current_user_id, + GeneratedResource.deleted_at.is_(None), + ) + ) + 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_type not in ["image", "video"]: + raise ValueError(f"资源 {resource_id} 不支持的资源类型,仅支持image和video") + + if not storage_path: + raise ValueError(f"资源 {resource_id} 本地存储路径为空") + + for advertiser_id in advertiser_ids: + result = await _upload_to_juliang( + oauth_id, storage_path, resource_type, advertiser_id, resource, db, current_user_id + ) + all_upload_results.append(result) + if result.get("success"): + success_count += 1 + else: + fail_count += 1 + + await db.commit() + + return { + "success_count": success_count, + "fail_count": fail_count, + "total_count": len(resource_ids) * len(advertiser_ids), + "results": all_upload_results, + } + + +async def _upload_to_juliang( + oauth_id: str, + storage_path: str, + resource_type: str, + advertiser_id: str, + resource: GeneratedResource, + db: AsyncSession, + current_user_id: str, +) -> any: + filename = os.path.basename(storage_path) + + if resource_type == "image": + if resource.file_size_bytes > 5 * 1024 * 1024: + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": False, + "error": "图片大小超过5M,不支持上传", + } + + with open(storage_path, "rb") as f: + file_content = f.read() + image_signature = hashlib.md5(file_content).hexdigest() + + data = { + "advertiser_id": advertiser_id, + "upload_type": "UPLOAD_BY_FILE", + "image_signature": image_signature, + "filename": filename, + } + + files = { + "image_file": (filename, file_content, "image/png"), + } + + response = await douyin_api.upload_image_material(oauth_id, data, files) + + if response["code"] != 0: + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": False, + "error": response.get("message", "上传失败"), + } + + image_id = response["data"]["id"] + material_id = response["data"]["material_id"] + + existing = await db.execute( + select(ResourcesMaterial).where( + ResourcesMaterial.oauth_id == oauth_id, + ResourcesMaterial.advertiser_id == advertiser_id, + ResourcesMaterial.material_id == str(material_id), + ResourcesMaterial.upload_id == str(image_id), + ResourcesMaterial.deleted_at.is_(None), + ) + ) + existing = existing.scalar_one_or_none() + + if existing: + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": True, + "material_id": str(material_id), + "upload_id": str(image_id), + "message": "素材已存在,跳过添加", + } + + db.add(ResourcesMaterial( + id=generate_id(), + oauth_id=oauth_id, + advertiser_id=advertiser_id, + target_table="generated_resources", + target_id=resource.id, + material_id=str(material_id), + upload_id=str(image_id), + resource_type=resource_type, + user_id=current_user_id, + )) + + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": True, + "material_id": str(material_id), + "upload_id": str(image_id), + } + + elif resource_type == "video": + if resource.file_size_bytes > 500 * 1024 * 1024: + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": False, + "error": "视频大小超过500M,不支持上传", + } + + with open(storage_path, "rb") as f: + file_content = f.read() + video_signature = hashlib.md5(file_content).hexdigest() + + data = { + "advertiser_id": advertiser_id, + "upload_type": "UPLOAD_BY_FILE", + "video_signature": video_signature, + "filename": filename, + } + + files = { + "video_file": (filename, file_content, "video/mp4"), + } + + response = await douyin_api.upload_video_material(oauth_id, data, files) + + if response["code"] != 0: + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": False, + "error": response.get("message", "上传失败"), + } + + video_id = response["data"]["video_id"] + material_id = response["data"]["material_id"] + + existing = await db.execute( + select(ResourcesMaterial).where( + ResourcesMaterial.oauth_id == oauth_id, + ResourcesMaterial.advertiser_id == advertiser_id, + ResourcesMaterial.material_id == str(material_id), + ResourcesMaterial.upload_id == str(video_id), + ResourcesMaterial.deleted_at.is_(None), + ) + ) + existing = existing.scalar_one_or_none() + + if existing: + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": True, + "material_id": str(material_id), + "upload_id": str(video_id), + "message": "素材已存在,跳过添加", + } + + db.add(ResourcesMaterial( + id=generate_id(), + oauth_id=oauth_id, + advertiser_id=advertiser_id, + target_table="generated_resources", + target_id=resource.id, + material_id=str(material_id), + upload_id=str(video_id), + resource_type=resource_type, + user_id=current_user_id, + )) + + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": True, + "material_id": str(material_id), + "upload_id": str(video_id), + } + + else: + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": False, + "error": f"不支持的资源类型: {resource_type}", + } + + +async def _upload_to_kuaishou( + access_token: str, + storage_path: str, + resource_type: str, + advertiser_id: str, + resource: GeneratedResource, + db: AsyncSession, + current_user_id: str, +) -> dict: + filename = os.path.basename(storage_path) + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": False, + "error": "快手平台上传接口暂未开通", + } + + +async def _upload_to_tencent( + access_token: str, + storage_path: str, + resource_type: str, + advertiser_id: str, + resource: GeneratedResource, + db: AsyncSession, + current_user_id: str, +) -> dict: + filename = os.path.basename(storage_path) + return { + "resource_id": resource.id, + "advertiser_id": advertiser_id, + "filename": filename, + "success": False, + "error": "腾讯平台上传接口暂未开通", + } \ No newline at end of file diff --git a/video-gen-api/app/services/user_oauth_app_service.py b/video-gen-api/app/services/user_oauth_app_service.py index 5841de08..3223f893 100644 --- a/video-gen-api/app/services/user_oauth_app_service.py +++ b/video-gen-api/app/services/user_oauth_app_service.py @@ -75,7 +75,7 @@ async def create_user_oauth_app( app_id=app_id, secret=secret, open_type=open_type, - count=count, + max_count=count, auth_url=auth_url, company=company, create_by=create_by, @@ -109,7 +109,7 @@ async def update_user_oauth_app( if status is not None: app.status = status if count is not None: - app.count = count + app.max_count = count if auth_url is not None: app.auth_url = auth_url if company is not None: diff --git a/video-gen-api/app/services/user_oauth_service.py b/video-gen-api/app/services/user_oauth_service.py index 897ae203..3ae6ea18 100644 --- a/video-gen-api/app/services/user_oauth_service.py +++ b/video-gen-api/app/services/user_oauth_service.py @@ -10,20 +10,6 @@ from app.models.user_oauth import UserOAuth from app.models.user_oauth_app import UserOAuthApp from app.utils.id_gen import generate_id - -OAUTH_TYPE_CONFIG = { - 1: {"port_type": 1, "name": "千川", "app_type": "juliang_qianchuan"}, - 2: {"port_type": 1, "name": "广告", "app_type": "juliang_ad"}, - 3: {"port_type": 1, "name": "本地推", "app_type": "juliang_ad"}, - 4: {"port_type": 1, "name": "星图", "app_type": "juliang_ad"}, - 5: {"port_type": 2, "name": "快手代理商", "app_type": "kuaishou"}, - 6: {"port_type": 3, "name": "巨量星图", "app_type": "juliang_ad"}, - 7: {"port_type": 4, "name": "巨量服务单", "app_type": "juliang_ad"}, - 8: {"port_type": 4, "name": "腾讯服务单", "app_type": "tencent"}, - 9: {"port_type": 5, "name": "腾讯营销K2", "app_type": "tencent"}, - 10: {"port_type": 5, "name": "腾讯营销K3", "app_type": "tencent"}, -} - #随机获取一个可用的应用配置 async def get_available_app(open_type: int, db: AsyncSession) -> dict: # 从 user_oauth_app 表查询可用应用 @@ -54,7 +40,7 @@ async def get_available_app(open_type: int, db: AsyncSession) -> dict: count = count_result.scalar() or 0 # 检查是否达到最大授权数 - max_users = app.count + max_users = app.max_count if count < max_users: available_apps.append({ "app_id": app.app_id, @@ -266,5 +252,52 @@ async def get_juliang_token(app_id: str, secret: str, code: str, open_type: int, async def get_kuaishou_token(app_id: str, secret: str, code: str, oauth_type: int, user_id: str, db: AsyncSession) -> dict: return "未配置" + + async def get_tencent_token(app_id: str, secret: str, code: str, oauth_type: int, user_id: str, db: AsyncSession) -> str: - return "未配置" \ No newline at end of file + return "未配置" + + +async def get_oauth_list( + user_id: str, + db: AsyncSession, + account_userid: str | None = None, + open_type: int | None = None, + account_id: str | None = None, + page: int = 1, + page_size: int = 10, +) -> dict: + if page < 1: + page = 1 + if page_size < 1: + page_size = 10 + + query = select(UserOAuth).where( + UserOAuth.user_id == user_id, + UserOAuth.deleted_at.is_(None), + ) + + if account_userid: + query = query.where(UserOAuth.account_userid == account_userid) + if open_type: + query = query.where(UserOAuth.open_type == open_type) + if account_id: + query = query.where(UserOAuth.account_id == account_id) + + query = query.order_by(UserOAuth.created_at.desc()) + + total_result = await db.execute(query.with_only_columns(UserOAuth.id)) + total = len(total_result.scalars().all()) + + offset = (page - 1) * page_size + query = query.offset(offset).limit(page_size) + + result = await db.execute(query) + oauth_list = result.scalars().all() + + return { + "data": oauth_list, + "total": total, + "page": page, + "page_size": page_size, + } \ No newline at end of file diff --git a/video-gen-api/app/utils/douyinApi.py b/video-gen-api/app/utils/douyinApi.py index 8ad984ca..cd85c8c7 100644 --- a/video-gen-api/app/utils/douyinApi.py +++ b/video-gen-api/app/utils/douyinApi.py @@ -3,18 +3,53 @@ from typing import Any, Dict, Optional from app.utils.douyinRequest import DouyinRequest from app.models.user_oauth import UserOAuth -class DouyinApi: - def __init__(self, request: DouyinRequest): - self.request = request - async def get_advertiser_list(self, oauth_id: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: +class DouyinApi: + def __init__(self): + self.request = DouyinRequest() + + async def get_advertiser_by_agent(self, oauth_id: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: if not oauth_id: raise RuntimeError('OAuth ID is not set.') - url = "" + url = "https://api.oceanengine.com/open_api/2/agent/advertiser/select/" return await self.request.request_with_token_with_context( oauth_id, url, 'GET', {'params': params or {}} + ) + + async def upload_image_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.') + + url = "https://api.oceanengine.com/open_api/2/file/image/ad/" + options: Dict[str, Any] = {} + if data: + options['data'] = data + if files: + options['files'] = files + return await self.request.request_with_token_with_context( + oauth_id, + url, + 'POST', + 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.') + + url = "https://api.oceanengine.com/open_api/2/file/video/ad/" + options: Dict[str, Any] = {} + if data: + options['data'] = data + if files: + options['files'] = files + return await self.request.request_with_token_with_context( + oauth_id, + url, + 'POST', + options ) \ No newline at end of file diff --git a/video-gen-api/app/utils/douyinRequest.py b/video-gen-api/app/utils/douyinRequest.py index 2fbce762..da45bc5d 100644 --- a/video-gen-api/app/utils/douyinRequest.py +++ b/video-gen-api/app/utils/douyinRequest.py @@ -187,24 +187,31 @@ class DouyinRequest: url: str, method: str = 'GET', options: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + ) -> Any: options = options or {} token = await self.get_access_token(oauth_id) - for i in range(1, 6): + for i in range(1, 2): try: headers = options.get('headers', {}).copy() headers['Access-Token'] = token - headers.setdefault('Content-Type', 'application/json') + + has_files = 'files' in options + if has_files: + # 移除可能错误设置的 Content-Type,让库自动生成 multipart 头 + headers.pop('Content-Type', None) + else: + headers.setdefault('Content-Type', 'application/json') + options['headers'] = headers - + response = await self.client.request(method, url, **options) response.raise_for_status() - + try: data = response.json() except json.JSONDecodeError: - data = {'code': 0, 'data': response.text} + data = {'code': 0, 'data': response.text, 'msg':'JSON解析失败'} if 'code' not in data: await asyncio.sleep(i * 5) @@ -236,9 +243,18 @@ class DouyinRequest: continue res = json.dumps(data) if 'data' in locals() else '' + + options_log = {} + if options: + for key, value in options.items(): + if key == 'files': + options_log[key] = {k: (v[0], 'bytes_content', v[2]) for k, v in value.items()} + else: + options_log[key] = value + raise RuntimeError( f'DouYin API request failed after 5 retries. ' - f'url:{url};oauthId:{oauth_id};options:{json.dumps(options)};response:{res}' + f'url:{url};oauthId:{oauth_id};options:{json.dumps(options_log)};response:{res}' ) # 无token请求 @@ -250,7 +266,7 @@ class DouyinRequest: ) -> Dict[str, Any]: options = options or {} - for i in range(1, 6): + for i in range(1, 2): try: headers = options.get('headers', {}).copy() headers.setdefault('Content-Type', 'application/json')