新增素材更新功能

This commit is contained in:
18610128193
2026-06-23 14:16:31 +08:00
parent b88546ab66
commit 242030c50b
11 changed files with 1109 additions and 7 deletions
+4
View File
@@ -22,6 +22,8 @@ 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
from app.api.v1.material_consumption import router as material_consumption_router
from app.api.v1.open_type import router as open_type_router
from app.api.admin import router as admin_module_router
api_router = APIRouter()
@@ -47,4 +49,6 @@ 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)
api_router.include_router(material_consumption_router)
api_router.include_router(open_type_router)
api_router.include_router(admin_module_router)
@@ -0,0 +1,301 @@
from typing import Any, Optional
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Query, Depends, Body
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
from app.models.material_cost import MaterialCost
from app.models.user_oauth import UserOAuth
from app.models.user_oauth_account import UserOAuthAccount
from app.dependencies import get_current_user, get_db
from app.services.material_consumption_queue import sync_all_advertisers_consumption, _fetch_and_save_consumption
router = APIRouter(prefix="/material-consumption", tags=["material-consumption"])
@router.get(
"/list",
summary="查询素材消耗列表",
description="查询当前用户的素材消耗列表",
)
async def get_consumption_list(
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
advertiser_id: Optional[str] = Query(None, description="广告主ID筛选"),
start_date: Optional[str] = Query(None, description="开始日期"),
end_date: Optional[str] = Query(None, description="结束日期"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
offset = (page - 1) * page_size
query = (
select(MaterialCost)
.join(
UserOAuth,
MaterialCost.oauth_id == UserOAuth.id,
)
.where(
UserOAuth.user_id == current_user.id,
MaterialCost.deleted_at.is_(None),
UserOAuth.deleted_at.is_(None),
)
)
if advertiser_id:
query = query.where(MaterialCost.advertiser_id == advertiser_id)
if start_date:
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d").date()
query = query.where(MaterialCost.consume_date >= start_date_obj)
if end_date:
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d").date()
query = query.where(MaterialCost.consume_date <= end_date_obj)
result = await db.execute(
query.order_by(MaterialCost.consume_date.desc())
.offset(offset)
.limit(page_size)
)
consumptions = result.scalars().all()
count_query = (
select(func.count(MaterialCost.id))
.join(
UserOAuth,
MaterialCost.oauth_id == UserOAuth.id,
)
.where(
UserOAuth.user_id == current_user.id,
MaterialCost.deleted_at.is_(None),
UserOAuth.deleted_at.is_(None),
)
)
if advertiser_id:
count_query = count_query.where(MaterialCost.advertiser_id == advertiser_id)
if start_date:
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d").date()
count_query = count_query.where(MaterialCost.consume_date >= start_date_obj)
if end_date:
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d").date()
count_query = count_query.where(MaterialCost.consume_date <= end_date_obj)
total_result = await db.execute(count_query)
total = total_result.scalar_one()
return {
"code": 0,
"data": [
{
"id": consumption.id,
"advertiser_id": consumption.advertiser_id,
"material_id": consumption.material_id,
"consume_date": consumption.consume_date.isoformat() if consumption.consume_date else None,
"stat_cost": consumption.stat_cost,
"show_cnt": consumption.show_cnt,
"cpm_platform": consumption.cpm_platform,
"click_cnt": consumption.click_cnt,
"ctr": consumption.ctr,
"cpc_platform": consumption.cpc_platform,
"convert_cnt": consumption.convert_cnt,
"conversion_cost": consumption.conversion_cost,
"conversion_rate": consumption.conversion_rate,
"deep_convert_cnt": consumption.deep_convert_cnt,
"deep_convert_cost": consumption.deep_convert_cost,
"deep_convert_rate": consumption.deep_convert_rate,
"active": consumption.active,
"active_cost": consumption.active_cost,
"active_rate": consumption.active_rate,
"active_register": consumption.active_register,
"active_register_cost": consumption.active_register_cost,
"active_register_rate": consumption.active_register_rate,
"attribution_next_day_open_cnt": consumption.attribution_next_day_open_cnt,
"attribution_next_day_open_cost": consumption.attribution_next_day_open_cost,
"attribution_next_day_open_rate": consumption.attribution_next_day_open_rate,
"active_pay": consumption.active_pay,
"active_pay_cost": consumption.active_pay_cost,
"active_pay_rate": consumption.active_pay_rate,
"phone": consumption.phone,
"form": consumption.form,
"download_start": consumption.download_start,
"form_submit": consumption.form_submit,
"button": consumption.button,
"view": consumption.view,
"message": consumption.message,
"consult": consumption.consult,
"consult_effective": consumption.consult_effective,
"shopping": consumption.shopping,
"customer_effective": consumption.customer_effective,
"attribution_game_in_app_ltv_1day": consumption.attribution_game_in_app_ltv_1day,
"attribution_game_in_app_roi_1day": consumption.attribution_game_in_app_roi_1day,
"loan_completion": consumption.loan_completion,
"loan_completion_cost": consumption.loan_completion_cost,
"loan_completion_rate": consumption.loan_completion_rate,
"loan_credit": consumption.loan_credit,
"loan_credit_cost": consumption.loan_credit_cost,
"loan_credit_rate": consumption.loan_credit_rate,
"in_app_order_gmv": consumption.in_app_order_gmv,
"in_app_order_roi": consumption.in_app_order_roi,
"in_app_pay_gmv": consumption.in_app_pay_gmv,
"in_app_pay_roi": consumption.in_app_pay_roi,
"total_play": consumption.total_play,
"valid_play": consumption.valid_play,
"valid_play_cost": consumption.valid_play_cost,
"valid_play_rate": consumption.valid_play_rate,
"valid_play_of_mille": consumption.valid_play_of_mille,
"valid_play_cost_of_mille": consumption.valid_play_cost_of_mille,
"average_play_time_per_play": consumption.average_play_time_per_play,
"play_over_rate": consumption.play_over_rate,
"dy_like": consumption.dy_like,
"dy_comment": consumption.dy_comment,
"dy_share": consumption.dy_share,
"report_cnt": consumption.report_cnt,
"created_at": consumption.created_at,
}
for consumption in consumptions
],
"pagination": {
"page": page,
"page_size": page_size,
"total": total,
},
}
@router.get(
"/sync",
summary="手动同步素材消耗",
description="手动触发素材消耗同步任务,加入队列顺序执行",
)
async def sync_consumption(
date: str = Query(None, description="同步日期,默认为昨天"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
if date is None:
date = (datetime.now(timezone.utc) - timedelta(days=1)).strftime("%Y-%m-%d")
# 测试用,手动触发同步素材消耗任务
result = await _fetch_and_save_consumption("0019eb9f130027c05b8", "1863675913228435", date)
return {
"code": 0,
"message": result,
}
result = await sync_all_advertisers_consumption(date)
return {
"code": 0,
"message": result["message"],
}
@router.get(
"/fields",
summary="获取素材消耗字段描述",
description="返回素材消耗表每个字段对应的中文描述,帮助前端理解字段含义",
)
async def get_consumption_fields() -> Any | dict:
fields = [
{"field": "id", "description": "主键"},
{"field": "advertiser_id", "description": "广告主id"},
{"field": "material_id", "description": "素材id"},
{"field": "consume_date", "description": "消耗日期"},
{"field": "stat_cost", "description": "消耗金额"},
{"field": "show_cnt", "description": "展示数"},
{"field": "cpm_platform", "description": "平均千次展现费用(元)"},
{"field": "click_cnt", "description": "点击数"},
{"field": "ctr", "description": "点击率"},
{"field": "cpc_platform", "description": "平均点击单价(元)"},
{"field": "convert_cnt", "description": "转化数"},
{"field": "conversion_cost", "description": "平均转化成本(元)"},
{"field": "conversion_rate", "description": "转化率"},
{"field": "deep_convert_cnt", "description": "深度转化数"},
{"field": "deep_convert_cost", "description": "深度转化成本(元)"},
{"field": "deep_convert_rate", "description": "深度转化率"},
{"field": "active", "description": "激活数"},
{"field": "active_cost", "description": "激活成本(元)"},
{"field": "active_rate", "description": "激活率"},
{"field": "active_register", "description": "注册数"},
{"field": "active_register_cost", "description": "注册成本(元)"},
{"field": "active_register_rate", "description": "注册率"},
{"field": "attribution_next_day_open_cnt", "description": "次留数"},
{"field": "attribution_next_day_open_cost", "description": "次留成本"},
{"field": "attribution_next_day_open_rate", "description": "次留率"},
{"field": "active_pay", "description": "首次付费数"},
{"field": "active_pay_cost", "description": "首次付费成本(元)"},
{"field": "active_pay_rate", "description": "首次付费率"},
{"field": "phone", "description": "点击电话按钮"},
{"field": "form", "description": "用户在门店落地页多线沟通提交表单的次数"},
{"field": "download_start", "description": "用户点击下载开始的次数"},
{"field": "form_submit", "description": "用户查看附加创意后,提交表单的次数"},
{"field": "button", "description": "用户点击按钮button的次数"},
{"field": "view", "description": "用户在关键页面的浏览次数"},
{"field": "message", "description": "用户点击短信咨询的次数"},
{"field": "consult", "description": "用户点击在线咨询按钮的次数"},
{"field": "consult_effective", "description": "用户在门店落地页多线沟通的在线咨询中有效咨询的次数"},
{"field": "shopping", "description": "用户购买商品的次数"},
{"field": "customer_effective", "description": "有效获客"},
{"field": "attribution_game_in_app_ltv_1day", "description": "当日付费金额"},
{"field": "attribution_game_in_app_roi_1day", "description": "当日付费ROI"},
{"field": "loan_completion", "description": "完件数(互联网金融-贷款行业中,用户成功提交贷款额度申请的行为)"},
{"field": "loan_completion_cost", "description": "完件成本(元)"},
{"field": "loan_completion_rate", "description": "完件率"},
{"field": "loan_credit", "description": "授信数(互联网金融-贷款行业中,用户提交贷款额度申请后,客户审批通过,给予用户可贷款的额度)"},
{"field": "loan_credit_cost", "description": "授信成本(元)"},
{"field": "loan_credit_rate", "description": "授信率"},
{"field": "in_app_order_gmv", "description": "引流电商订单GMV(当您使用\"in_app_order\"事件回传订单金额时,对应的GMV金额)"},
{"field": "in_app_order_roi", "description": "引流电商订单ROI"},
{"field": "in_app_pay_gmv", "description": "引流电商支付GMV"},
{"field": "in_app_pay_roi", "description": "引流电商支付ROI"},
{"field": "total_play", "description": "播放量(播放时间大于0S的数量,在某些蜂窝网络环境下,需要您手动点击开始才会开始播放,因此有时播放数小于展示数)"},
{"field": "valid_play", "description": "有效播放数"},
{"field": "valid_play_cost", "description": "有效播放成本(元)"},
{"field": "valid_play_rate", "description": "有效播放率"},
{"field": "valid_play_of_mille", "description": "千次有效播放数"},
{"field": "valid_play_cost_of_mille", "description": "千次有效播放成本(元)"},
{"field": "average_play_time_per_play", "description": "平均单次播放时长"},
{"field": "play_over_rate", "description": "完播率"},
{"field": "dy_like", "description": "点赞数"},
{"field": "dy_comment", "description": "评论量"},
{"field": "dy_share", "description": "分享量"},
{"field": "report_cnt", "description": "举报数"},
{"field": "created_at", "description": "创建时间"},
]
return {
"code": 0,
"data": fields,
}
@router.get(
"/advertisers",
summary="获取用户的广告主列表",
description="获取当前用户有权限的广告主列表,用于筛选",
)
async def get_advertisers(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
result = await db.execute(
select(UserOAuthAccount.advertiser_id)
.join(UserOAuth, UserOAuthAccount.oauth_id == UserOAuth.id)
.where(
UserOAuth.user_id == current_user.id,
UserOAuth.deleted_at.is_(None),
UserOAuthAccount.deleted_at.is_(None),
)
.distinct()
)
advertisers = result.scalars().all()
return {
"code": 0,
"data": [{"advertiser_id": str(aid)} for aid in advertisers],
}
+262
View File
@@ -0,0 +1,262 @@
from typing import Any, Optional
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_admin_user, get_db
from app.models.user import User
from app.models.open_type import OpenType
from app.schemas.open_type import (
OpenTypeCreate,
OpenTypeUpdate,
OpenTypeListResponse,
OpenTypeResponse,
)
from app.utils.id_gen import generate_id
router = APIRouter(prefix="/open-type", tags=["open-type"])
@router.get(
"/list",
summary="获取开户方式列表",
description="获取开户方式列表,支持分页和筛选",
response_model=OpenTypeListResponse,
)
async def get_open_type_list(
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
type_name: Optional[str] = Query(None, description="标题名称筛选"),
open_type: Optional[int] = Query(None, description="开户方式id筛选"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
offset = (page - 1) * page_size
query = select(OpenType).where(OpenType.deleted_at.is_(None))
if type_name:
query = query.where(OpenType.type_name.like(f"%{type_name}%"))
if open_type:
query = query.where(OpenType.open_type == open_type)
result = await db.execute(
query.order_by(OpenType.open_type.asc())
.offset(offset)
.limit(page_size)
)
open_types = result.scalars().all()
count_query = select(func.count(OpenType.id)).where(OpenType.deleted_at.is_(None))
if type_name:
count_query = count_query.where(OpenType.type_name.like(f"%{type_name}%"))
if open_type:
count_query = count_query.where(OpenType.open_type == open_type)
total_result = await db.execute(count_query)
total = total_result.scalar_one()
return {
"code": 0,
"message": "查询成功",
"data": [
{
"id": item.id,
"type_name": item.type_name,
"open_type": item.open_type,
"description": item.description,
"thumb": item.thumb,
"created_at": item.created_at,
"updated_at": item.updated_at,
}
for item in open_types
],
"pagination": {
"page": page,
"page_size": page_size,
"total": total,
"total_pages": (total + page_size - 1) // page_size,
},
}
@router.get(
"/{id}",
summary="获取开户方式详情",
description="根据ID获取开户方式详情",
response_model=OpenTypeResponse,
)
async def get_open_type_detail(
id: str,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
result = await db.execute(
select(OpenType).where(OpenType.id == id, OpenType.deleted_at.is_(None))
)
open_type = result.scalar_one_or_none()
if not open_type:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="开户方式不存在",
)
return {
"code": 0,
"message": "查询成功",
"data": {
"id": open_type.id,
"type_name": open_type.type_name,
"open_type": open_type.open_type,
"description": open_type.description,
"thumb": open_type.thumb,
"created_at": open_type.created_at,
"updated_at": open_type.updated_at,
},
}
@router.post(
"/",
summary="创建开户方式",
description="创建新的开户方式",
response_model=OpenTypeResponse,
)
async def create_open_type(
req: OpenTypeCreate = Body(...),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
existing = await db.execute(
select(OpenType).where(OpenType.open_type == req.open_type, OpenType.deleted_at.is_(None))
)
if existing.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"开户方式ID {req.open_type} 已存在",
)
open_type = OpenType(
id=generate_id(),
type_name=req.type_name,
open_type=req.open_type,
description=req.description,
thumb=req.thumb,
)
db.add(open_type)
await db.commit()
await db.refresh(open_type)
return {
"code": 0,
"message": "创建成功",
"data": {
"id": open_type.id,
"type_name": open_type.type_name,
"open_type": open_type.open_type,
"description": open_type.description,
"thumb": open_type.thumb,
"created_at": open_type.created_at,
"updated_at": open_type.updated_at,
},
}
@router.put(
"/{id}",
summary="更新开户方式",
description="更新指定的开户方式",
response_model=OpenTypeResponse,
)
async def update_open_type(
id: str,
req: OpenTypeUpdate = Body(...),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
result = await db.execute(
select(OpenType).where(OpenType.id == id, OpenType.deleted_at.is_(None))
)
open_type = result.scalar_one_or_none()
if not open_type:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="开户方式不存在",
)
if req.open_type is not None and req.open_type != open_type.open_type:
existing = await db.execute(
select(OpenType).where(
OpenType.open_type == req.open_type,
OpenType.id != id,
OpenType.deleted_at.is_(None),
)
)
if existing.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"开户方式ID {req.open_type} 已存在",
)
if req.type_name is not None:
open_type.type_name = req.type_name
if req.open_type is not None:
open_type.open_type = req.open_type
if req.description is not None:
open_type.description = req.description
if req.thumb is not None:
open_type.thumb = req.thumb
await db.commit()
await db.refresh(open_type)
return {
"code": 0,
"message": "更新成功",
"data": {
"id": open_type.id,
"type_name": open_type.type_name,
"open_type": open_type.open_type,
"description": open_type.description,
"thumb": open_type.thumb,
"created_at": open_type.created_at,
"updated_at": open_type.updated_at,
},
}
@router.delete(
"/{id}",
summary="删除开户方式",
description="软删除指定的开户方式",
response_model=OpenTypeResponse,
)
async def delete_open_type(
id: str,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
result = await db.execute(
select(OpenType).where(OpenType.id == id, OpenType.deleted_at.is_(None))
)
open_type = result.scalar_one_or_none()
if not open_type:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="开户方式不存在",
)
open_type.deleted_at = func.now()
await db.commit()
return {
"code": 0,
"message": "删除成功",
"data": None,
}
@@ -222,6 +222,14 @@ async def async_batch_upload_material(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
# param = {
# "account_ids" : json.dumps([1836693172153543]),
# }
# account_info = await DouyinApi().get_account_info("0019eb9f130027c05b8", param)
# return account_info
if not req.tasks:
return {
"code": 0,