From 15179cd2a449caad6229db332adc10fbe1042f76 Mon Sep 17 00:00:00 2001 From: 18610128193 <10574456+chenweiqiang-123@user.noreply.gitee.com> Date: Wed, 17 Jun 2026 14:19:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=89=8D=E6=B5=8B=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-api/app/api/v1/__init__.py | 2 + video-gen-api/app/api/v1/pre_test_template.py | 294 ++++++++++++++++++ video-gen-api/app/api/v1/user_oauth.py | 4 +- .../app/schemas/pre_test_template.py | 111 +++++++ .../app/services/pre_test_template_service.py | 258 +++++++++++++++ 5 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 video-gen-api/app/api/v1/pre_test_template.py create mode 100644 video-gen-api/app/schemas/pre_test_template.py create mode 100644 video-gen-api/app/services/pre_test_template_service.py diff --git a/video-gen-api/app/api/v1/__init__.py b/video-gen-api/app/api/v1/__init__.py index 6bc09321..7430c445 100644 --- a/video-gen-api/app/api/v1/__init__.py +++ b/video-gen-api/app/api/v1/__init__.py @@ -21,6 +21,7 @@ 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 +from app.api.v1.pre_test_template import router as pre_test_template_router api_router = APIRouter() api_router.include_router(auth_router) @@ -44,3 +45,4 @@ 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) +api_router.include_router(pre_test_template_router) diff --git a/video-gen-api/app/api/v1/pre_test_template.py b/video-gen-api/app/api/v1/pre_test_template.py new file mode 100644 index 00000000..c8568e18 --- /dev/null +++ b/video-gen-api/app/api/v1/pre_test_template.py @@ -0,0 +1,294 @@ +import json +from typing import Any, Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.models.pre_test_template import PreTestTemplate + +from app.dependencies import get_current_user, get_db +from app.models.user import User +from app.schemas.pre_test_template import ( + PreTestTemplateCreate, + PreTestTemplateListResponse, + PreTestTemplateOut, + PreTestTemplateUpdate, +) +from app.services.pre_test_template_service import ( + create_pre_test_template, + delete_pre_test_template, + get_default_template, + get_pre_test_template, + get_pre_test_template_list, + update_pre_test_template, +) + +router = APIRouter(prefix="/pre-test-template", tags=["前测模板"]) + + +def _template_to_dict(template): + return { + "id": template.id, + "name": template.name, + "note": template.note, + "platform": template.platform, + "external_action": template.external_action, + "cpa_bid": template.cpa_bid, + "audience_gender": template.audience_gender, + "audience_age": json.loads(template.audience_age) if template.audience_age else None, + "audience_region": json.loads(template.audience_region) if template.audience_region else None, + "audience_network": json.loads(template.audience_network) if template.audience_network else None, + "cus_name": template.cus_name, + "pricing_type": template.pricing_type, + "cost_cap": template.cost_cap, + "target_cost": template.target_cost, + "nobid": template.nobid, + "cpc_bid": template.cpc_bid, + "budget": template.budget, + "is_default": template.is_default, + "user_id": template.user_id, + "created_at": template.created_at, + "updated_at": template.updated_at, + } + + +@router.post( + "/create", + summary="创建前测模板", + description="创建一个新的前测模板", +) +async def create_template( + req: PreTestTemplateCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any: + try: + #新增一个判断,如果模板名称重复,提示用户修改 + result = await db.execute( + select(PreTestTemplate).where( + PreTestTemplate.name == req.name, + PreTestTemplate.user_id == current_user.id, + PreTestTemplate.deleted_at.is_(None), + ) + ) + existing_template = result.scalar() + if existing_template: + raise ValueError("模板名称已存在") + + template = await create_pre_test_template( + user_id=current_user.id, + db=db, + name=req.name, + note=req.note, + platform=req.platform, + external_action=req.external_action, + cpa_bid=req.cpa_bid, + audience_gender=req.audience_gender, + audience_age=req.audience_age, + audience_region=req.audience_region, + audience_network=req.audience_network, + cus_name=req.cus_name, + pricing_type=req.pricing_type, + cost_cap=req.cost_cap, + target_cost=req.target_cost, + nobid=req.nobid, + cpc_bid=req.cpc_bid, + budget=req.budget, + is_default=req.is_default, + ) + + return { + "code": 0, + "message": "创建成功", + } + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"创建失败: {str(e)}", + ) + + +@router.get( + "/list", + summary="获取前测模板列表", + 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"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any: + try: + result = await get_pre_test_template_list( + user_id=current_user.id, + db=db, + platform=platform, + page=page, + page_size=page_size, + ) + + return { + "code": 0, + "message": "查询成功", + "data": [_template_to_dict(t) for t 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 Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"查询失败: {str(e)}", + ) + + +@router.get( + "/default", + summary="获取默认前测模板", + description="获取当前用户的默认前测模板", +) +async def get_default( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any: + try: + template = await get_default_template(current_user.id, db) + + if not template: + return { + "code": 0, + "message": "未设置默认模板", + "data": None, + } + + return { + "code": 0, + "message": "查询成功", + "data": _template_to_dict(template), + } + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"查询失败: {str(e)}", + ) + + +@router.get( + "/select/{template_id}", + summary="获取前测模板详情", + description="根据模板id获取前测模板详情", +) +async def get_template( + template_id: str, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any: + try: + template = await get_pre_test_template(template_id, current_user.id, db) + + if not template: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="模板不存在", + ) + + return { + "code": 0, + "message": "查询成功", + "data": _template_to_dict(template), + } + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"查询失败: {str(e)}", + ) + + +@router.post( + "/update/{template_id}", + summary="更新前测模板", + description="更新指定的前测模板", +) +async def update_template( + template_id: str, + req: PreTestTemplateUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any: + try: + update_data = req.dict(exclude_none=True) + template = await update_pre_test_template(template_id, current_user.id, db, **update_data) + + if not template: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="模板不存在", + ) + + return { + "code": 0, + "message": "更新成功", + "data": _template_to_dict(template), + } + except HTTPException: + raise + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"更新失败: {str(e)}", + ) + + +@router.get( + "/delete/{template_id}", + summary="删除前测模板", + description="软删除指定的前测模板", +) +async def delete_template( + template_id: str, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any: + try: + success = await delete_pre_test_template(template_id, current_user.id, db) + + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="模板不存在", + ) + + return { + "code": 0, + "message": "删除成功", + } + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"删除失败: {str(e)}", + ) diff --git a/video-gen-api/app/api/v1/user_oauth.py b/video-gen-api/app/api/v1/user_oauth.py index 72aa50f0..1bcca525 100644 --- a/video-gen-api/app/api/v1/user_oauth.py +++ b/video-gen-api/app/api/v1/user_oauth.py @@ -6,13 +6,12 @@ from app.dependencies import get_current_user, get_db from app.models.user import User from app.models.user_oauth import UserOAuth from app.models.user_oauth_app import UserOAuthApp -from app.schemas.user_oauth import RequestOAuthRequest, RequestOAuthResponse, UserOAuthOut +from app.schemas.user_oauth import RequestOAuthRequest, RequestOAuthResponse, UserOAuthOut, OAuthListResponse 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 router = APIRouter(prefix="/user-oauth", tags=["oauth"]) @@ -108,6 +107,7 @@ async def juliang_callback( "/oauth_list", summary="获取账户下所有授权列表", description="获取当前用户下所有授权账户列表,支持按授权登录账号、开户方式、授权账户id筛选", + response_model=OAuthListResponse, ) async def oauth_list( account_userid: str | None = Query(None, description="授权登录账号id"), diff --git a/video-gen-api/app/schemas/pre_test_template.py b/video-gen-api/app/schemas/pre_test_template.py new file mode 100644 index 00000000..c2037fe7 --- /dev/null +++ b/video-gen-api/app/schemas/pre_test_template.py @@ -0,0 +1,111 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field, field_validator + + +class PreTestTemplateCreate(BaseModel): + name: str = Field(..., description="模板名称") + note: Optional[str] = Field(None, description="模板备注") + platform: Optional[str] = Field(None, description="投放平台(AD/QIANCHUAN/LOCAL)") + external_action: Optional[str] = Field(None, description="转化目标") + cpa_bid: Optional[float] = Field(None, description="目标转化成本:[1, 10000]") + audience_gender: Optional[str] = Field(None, description="性别(ALL/MALE/FEMALE)") + audience_age: Optional[List[str]] = Field(None, description="受众年龄列表") + audience_region: Optional[List[int]] = Field(None, description="受众地区code列表") + audience_network: Optional[List[str]] = Field(None, description="网络类型列表") + cus_name: Optional[str] = Field(None, description="客户主体名称") + pricing_type: Optional[str] = Field(None, description="出价类型(OCPC/CPA/OCPM)") + cost_cap: Optional[bool] = Field(None, description="是否最优成本出价(仅AD支持)") + target_cost: Optional[bool] = Field(None, description="是否稳定成本出价(仅AD支持)") + nobid: Optional[bool] = Field(None, description="是否最大转化出价(仅AD支持)") + cpc_bid: Optional[float] = Field(None, description="目标点击成本:[1, 10000]") + budget: Optional[float] = Field(None, description="预算金额:[1, 10000]") + is_default: Optional[bool] = Field(None, description="是否设为默认模板") + + @field_validator('platform') + def validate_platform(cls, v): + if v is None: + return v + allowed = ["AD", "QIANCHUAN", "LOCAL"] + if v not in allowed: + raise ValueError(f"platform must be one of {allowed}") + return v + + @field_validator('pricing_type') + def validate_pricing_type(cls, v): + if v is None: + return v + allowed = ["OCPC", "CPA", "OCPM"] + if v not in allowed: + raise ValueError(f"pricing_type must be one of {allowed}") + return v + + @field_validator('audience_gender') + def validate_gender(cls, v): + if v is None: + return v + allowed = ["ALL", "MALE", "FEMALE"] + if v not in allowed: + raise ValueError(f"audience_gender must be one of {allowed}") + return v + + +class PreTestTemplateUpdate(BaseModel): + name: Optional[str] = Field(None, description="模板名称") + note: Optional[str] = Field(None, description="模板备注") + platform: Optional[str] = Field(None, description="投放平台") + external_action: Optional[str] = Field(None, description="转化目标") + cpa_bid: Optional[float] = Field(None, description="目标转化成本") + audience_gender: Optional[str] = Field(None, description="性别") + audience_age: Optional[List[str]] = Field(None, description="受众年龄列表") + audience_region: Optional[List[int]] = Field(None, description="受众地区code列表") + audience_network: Optional[List[str]] = Field(None, description="网络类型列表") + cus_name: Optional[str] = Field(None, description="客户主体名称") + pricing_type: Optional[str] = Field(None, description="出价类型") + cost_cap: Optional[bool] = Field(None, description="是否最优成本出价") + target_cost: Optional[bool] = Field(None, description="是否稳定成本出价") + nobid: Optional[bool] = Field(None, description="是否最大转化出价") + cpc_bid: Optional[float] = Field(None, description="目标点击成本") + budget: Optional[float] = Field(None, description="预算金额") + is_default: Optional[bool] = Field(None, description="是否设为默认模板") + + +class PreTestTemplateOut(BaseModel): + id: str = Field(..., description="主键") + name: str = Field(..., description="模板名称") + note: Optional[str] = Field(None, description="模板备注") + platform: Optional[str] = Field(None, description="投放平台") + external_action: Optional[str] = Field(None, description="转化目标") + cpa_bid: Optional[float] = Field(None, description="目标转化成本") + audience_gender: Optional[str] = Field(None, description="性别") + audience_age: Optional[List[str]] = Field(None, description="受众年龄列表") + audience_region: Optional[List[int]] = Field(None, description="受众地区code列表") + audience_network: Optional[List[str]] = Field(None, description="网络类型列表") + cus_name: Optional[str] = Field(None, description="客户主体名称") + pricing_type: Optional[str] = Field(None, description="出价类型") + cost_cap: Optional[bool] = Field(None, description="是否最优成本出价") + target_cost: Optional[bool] = Field(None, description="是否稳定成本出价") + nobid: Optional[bool] = Field(None, description="是否最大转化出价") + cpc_bid: Optional[float] = Field(None, description="目标点击成本") + budget: Optional[float] = Field(None, description="预算金额") + is_default: Optional[bool] = Field(None, description="是否默认模板") + user_id: str = Field(..., description="用户id") + created_at: datetime = Field(..., description="创建时间") + updated_at: datetime = Field(..., description="更新时间") + + model_config = {"from_attributes": True} + + +class PaginationInfo(BaseModel): + page: int = Field(..., description="当前页码") + page_size: int = Field(..., description="每页数量") + total: int = Field(..., description="总记录数") + total_pages: int = Field(..., description="总页数") + + +class PreTestTemplateListResponse(BaseModel): + code: int = Field(0, description="返回码") + message: str = Field("查询成功", description="返回消息") + data: List[PreTestTemplateOut] = Field(..., description="模板列表") + pagination: PaginationInfo = Field(..., description="分页信息") diff --git a/video-gen-api/app/services/pre_test_template_service.py b/video-gen-api/app/services/pre_test_template_service.py new file mode 100644 index 00000000..0bd4df58 --- /dev/null +++ b/video-gen-api/app/services/pre_test_template_service.py @@ -0,0 +1,258 @@ +import json +from typing import Optional + +from sqlalchemy import select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.pre_test_template import PreTestTemplate +from app.utils.id_gen import generate_id + + +async def create_pre_test_template( + user_id: str, + db: AsyncSession, + name: str, + note: Optional[str] = None, + platform: Optional[str] = None, + external_action: Optional[str] = None, + cpa_bid: Optional[float] = None, + audience_gender: Optional[str] = None, + audience_age: Optional[list] = None, + audience_region: Optional[list] = None, + audience_network: Optional[list] = None, + cus_name: Optional[str] = None, + pricing_type: Optional[str] = None, + cost_cap: Optional[bool] = None, + target_cost: Optional[bool] = None, + nobid: Optional[bool] = None, + cpc_bid: Optional[float] = None, + budget: Optional[float] = None, + is_default: Optional[bool] = None, +) -> PreTestTemplate: + if is_default: + await db.execute( + update(PreTestTemplate) + .where( + PreTestTemplate.user_id == user_id, + PreTestTemplate.is_default == True, + PreTestTemplate.deleted_at.is_(None), + ) + .values(is_default=False) + ) + + template = PreTestTemplate( + id=generate_id(), + user_id=user_id, + name=name, + note=note, + platform=platform, + external_action=external_action, + cpa_bid=cpa_bid, + audience_gender=audience_gender, + audience_age=json.dumps(audience_age) if audience_age else None, + audience_region=json.dumps(audience_region) if audience_region else None, + audience_network=json.dumps(audience_network) if audience_network else None, + cus_name=cus_name, + pricing_type=pricing_type, + cost_cap=cost_cap, + target_cost=target_cost, + nobid=nobid, + cpc_bid=cpc_bid, + budget=budget, + is_default=is_default, + ) + + db.add(template) + await db.commit() + await db.refresh(template) + return template + + +async def update_pre_test_template( + template_id: str, + user_id: str, + db: AsyncSession, + **kwargs, +) -> Optional[PreTestTemplate]: + result = await db.execute( + select(PreTestTemplate).where( + PreTestTemplate.id == template_id, + PreTestTemplate.user_id == user_id, + PreTestTemplate.deleted_at.is_(None), + ) + ) + template = result.scalar_one_or_none() + + if not template: + return None + + if kwargs.get("is_default"): + await db.execute( + update(PreTestTemplate) + .where( + PreTestTemplate.user_id == user_id, + PreTestTemplate.is_default == True, + PreTestTemplate.id != template_id, + PreTestTemplate.deleted_at.is_(None), + ) + .values(is_default=False) + ) + + update_data = {} + if "name" in kwargs: + update_data["name"] = kwargs["name"] + if "note" in kwargs: + update_data["note"] = kwargs["note"] + if "platform" in kwargs: + update_data["platform"] = kwargs["platform"] + if "external_action" in kwargs: + update_data["external_action"] = kwargs["external_action"] + if "cpa_bid" in kwargs: + update_data["cpa_bid"] = kwargs["cpa_bid"] + if "audience_gender" in kwargs: + update_data["audience_gender"] = kwargs["audience_gender"] + if "audience_age" in kwargs: + update_data["audience_age"] = json.dumps(kwargs["audience_age"]) if kwargs["audience_age"] else None + if "audience_region" in kwargs: + update_data["audience_region"] = json.dumps(kwargs["audience_region"]) if kwargs["audience_region"] else None + if "audience_network" in kwargs: + update_data["audience_network"] = json.dumps(kwargs["audience_network"]) if kwargs["audience_network"] else None + if "cus_name" in kwargs: + update_data["cus_name"] = kwargs["cus_name"] + if "pricing_type" in kwargs: + update_data["pricing_type"] = kwargs["pricing_type"] + if "cost_cap" in kwargs: + update_data["cost_cap"] = kwargs["cost_cap"] + if "target_cost" in kwargs: + update_data["target_cost"] = kwargs["target_cost"] + if "nobid" in kwargs: + update_data["nobid"] = kwargs["nobid"] + if "cpc_bid" in kwargs: + update_data["cpc_bid"] = kwargs["cpc_bid"] + if "budget" in kwargs: + update_data["budget"] = kwargs["budget"] + if "is_default" in kwargs: + update_data["is_default"] = kwargs["is_default"] + + if update_data: + await db.execute( + update(PreTestTemplate) + .where(PreTestTemplate.id == template_id) + .values(**update_data) + ) + await db.commit() + await db.refresh(template) + + return template + + +async def delete_pre_test_template( + template_id: str, + user_id: str, + db: AsyncSession, +) -> bool: + result = await db.execute( + select(PreTestTemplate).where( + PreTestTemplate.id == template_id, + PreTestTemplate.user_id == user_id, + PreTestTemplate.deleted_at.is_(None), + ) + ) + template = result.scalar_one_or_none() + + if not template: + return False + + from datetime import datetime + template.deleted_at = datetime.now() + await db.commit() + return True + + +async def get_pre_test_template( + template_id: str, + user_id: str, + db: AsyncSession, +) -> Optional[PreTestTemplate]: + result = await db.execute( + select(PreTestTemplate).where( + PreTestTemplate.id == template_id, + PreTestTemplate.user_id == user_id, + PreTestTemplate.deleted_at.is_(None), + ) + ) + return result.scalar_one_or_none() + + +async def get_pre_test_template_list( + user_id: str, + db: AsyncSession, + platform: Optional[str] = None, + page: int = 1, + page_size: int = 10, +) -> dict: + if page < 1: + page = 1 + if page_size < 1: + page_size = 10 + if page_size > 100: + page_size = 100 + + query = select(PreTestTemplate).where( + PreTestTemplate.user_id == user_id, + PreTestTemplate.deleted_at.is_(None), + ) + + if platform: + query = query.where(PreTestTemplate.platform == platform) + + query = query.order_by(PreTestTemplate.is_default.desc(), PreTestTemplate.created_at.desc()) + + total_result = await db.execute(query.with_only_columns(PreTestTemplate.id)) + total = len(total_result.scalars().all()) + + offset = (page - 1) * page_size + query = query.offset(offset).limit(page_size) + + result = await db.execute(query) + templates = result.scalars().all() + + return { + "data": templates, + "total": total, + "page": page, + "page_size": page_size, + } + + +async def get_default_template( + user_id: str, + db: AsyncSession, +) -> Optional[PreTestTemplate]: + # 如果有默认模板,返回默认模板 + # 如果有多条默认的模板,返回最新创建的模板 + # 如果没有设置默认模板,返回最新创建的模板 + result = await db.execute( + select(PreTestTemplate) + .where( + PreTestTemplate.user_id == user_id, + PreTestTemplate.is_default == True, + PreTestTemplate.deleted_at.is_(None), + ) + .order_by(PreTestTemplate.created_at.desc()) + ) + template = result.scalar_one_or_none() + + if template: + return template + + # 如果没有默认模板,返回最新创建的模板 + result = await db.execute( + select(PreTestTemplate) + .where( + PreTestTemplate.user_id == user_id, + PreTestTemplate.deleted_at.is_(None), + ) + .order_by(PreTestTemplate.created_at.desc()) + ) + return result.scalar_one_or_none() \ No newline at end of file