增加前测模板

This commit is contained in:
18610128193
2026-06-17 14:19:13 +08:00
parent 2a8855d9ea
commit 15179cd2a4
5 changed files with 667 additions and 2 deletions
+2
View File
@@ -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)
@@ -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)}",
)
+2 -2
View File
@@ -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"),
@@ -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="分页信息")
@@ -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()