diff --git a/video-gen-api/app/api/v1/material_consumption.py b/video-gen-api/app/api/v1/material_consumption.py index 3db0c85c..59f5e3b4 100644 --- a/video-gen-api/app/api/v1/material_consumption.py +++ b/video-gen-api/app/api/v1/material_consumption.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Optional, List from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Query, Depends, Body @@ -6,11 +6,13 @@ from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.models.user import User +from app.models.resources_material import ResourcesMaterial 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.dependencies import get_current_user, get_db, get_admin_user from app.services.material_consumption_queue import sync_all_advertisers_consumption, _fetch_and_save_consumption +from app.services.material_consumption_service import get_consumption_list, format_consumption_response router = APIRouter(prefix="/material-consumption", tags=["material-consumption"]) @@ -20,146 +22,26 @@ router = APIRouter(prefix="/material-consumption", tags=["material-consumption"] summary="查询素材消耗列表", description="查询当前用户的素材消耗列表", ) -async def get_consumption_list( +async def user_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="结束日期"), + consume_date: Optional[List[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), - ) + consumptions, total = await get_consumption_list( + db=db, + page=page, + page_size=page_size, + advertiser_id=advertiser_id, + consume_date=consume_date, + current_user=current_user, ) - 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 - ], + "data": format_consumption_response(consumptions), "pagination": { "page": page, "page_size": page_size, @@ -171,27 +53,55 @@ async def get_consumption_list( @router.get( "/sync", summary="手动同步素材消耗", - description="手动触发素材消耗同步任务,加入队列顺序执行", + description="手动触发当前用户授权的广告主的素材消耗同步任务,加入队列顺序执行", ) async def sync_consumption( date: str = Query(None, description="同步日期,默认为昨天"), + advertiser_id: Optional[str] = Query(None, description="指定广告主ID,不指定则同步所有授权的广告主"), 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) + from app.services.material_consumption_queue import material_consumption_queue + + query = ( + select(UserOAuth.id, UserOAuthAccount.advertiser_id) + .join(UserOAuthAccount, UserOAuth.id == UserOAuthAccount.oauth_id) + .join(ResourcesMaterial, UserOAuthAccount.advertiser_id == ResourcesMaterial.advertiser_id) + .where( + UserOAuth.user_id == current_user.id, + UserOAuth.deleted_at.is_(None), + UserOAuthAccount.deleted_at.is_(None), + ResourcesMaterial.deleted_at.is_(None), + ResourcesMaterial.material_id.is_not_(None), + ) + .distinct() + ) + + if advertiser_id: + query = query.where(UserOAuthAccount.advertiser_id == advertiser_id) + + result = await db.execute(query) + oauth_advertiser_pairs = result.all() + + if not oauth_advertiser_pairs: + return { + "code": 0, + "message": "没有找到可同步的广告主", + } + + for oauth_id, adv_id in oauth_advertiser_pairs: + await material_consumption_queue.enqueue({ + "oauth_id": oauth_id, + "advertiser_id": adv_id, + "date": date, + }) return { "code": 0, - "message": result["message"], + "message": f"已将 {len(oauth_advertiser_pairs)} 个广告主的消耗更新任务加入队列", } @@ -298,4 +208,47 @@ async def get_advertisers( return { "code": 0, "data": [{"advertiser_id": str(aid)} for aid in advertisers], + } + + +@router.get( + "/admin/list", + summary="管理员查询所有素材消耗列表", + description="管理员可查看系统中所有用户的素材消耗数据", +) +async def admin_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筛选"), + user_id: Optional[str] = Query(None, description="用户ID筛选"), + consume_date: Optional[List[str]] = Query(None, description="消耗日期范围,格式: ['开始日期','结束日期']"), + current_user: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + #需要判断管理员吗 + if not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="只有管理员才能查询所有素材消耗列表", + ) + + + consumptions, total = await get_consumption_list( + db=db, + page=page, + page_size=page_size, + advertiser_id=advertiser_id, + user_id=user_id, + consume_date=consume_date, + current_user=None, + ) + + return { + "code": 0, + "data": format_consumption_response(consumptions, include_oauth_id=True), + "pagination": { + "page": page, + "page_size": page_size, + "total": total, + }, } \ No newline at end of file diff --git a/video-gen-api/app/api/v1/open_type.py b/video-gen-api/app/api/v1/open_type.py index 7d134843..c8a0bf2e 100644 --- a/video-gen-api/app/api/v1/open_type.py +++ b/video-gen-api/app/api/v1/open_type.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Optional, List from fastapi import APIRouter, Body, Depends, HTTPException, Query, status from sqlalchemy import select, func @@ -18,6 +18,37 @@ from app.utils.id_gen import generate_id router = APIRouter(prefix="/open-type", tags=["open-type"]) +# 公共接口 - 无需登录 +@router.get( + "/open_type_all", + summary="查询所有开户方式", + description="查询所有开户方式,前端使用select框选择,无需登录", + dependencies=[], +) +async def get_open_type_public( + db: AsyncSession = Depends(get_db), +) -> Any | dict: + + result = await db.execute( + select(OpenType).where(OpenType.deleted_at.is_(None)) + ) + open_types = result.scalars().all() + return { + "code": 0, + "message": "查询成功", + "data": [ + { + "id": ot.id, + "type_name": ot.type_name, + "open_type": ot.open_type, + "description": ot.description, + "thumb": ot.thumb, + } + for ot in open_types + ], + } + + @router.get( "/list", summary="获取开户方式列表", @@ -83,7 +114,7 @@ async def get_open_type_list( @router.get( - "/{id}", + "/select/{id}", summary="获取开户方式详情", description="根据ID获取开户方式详情", response_model=OpenTypeResponse, @@ -120,7 +151,7 @@ async def get_open_type_detail( @router.post( - "/", + "/create", summary="创建开户方式", description="创建新的开户方式", response_model=OpenTypeResponse, @@ -167,7 +198,7 @@ async def create_open_type( @router.put( - "/{id}", + "/update/{id}", summary="更新开户方式", description="更新指定的开户方式", response_model=OpenTypeResponse, @@ -231,7 +262,7 @@ async def update_open_type( @router.delete( - "/{id}", + "/delete/{id}", summary="删除开户方式", description="软删除指定的开户方式", response_model=OpenTypeResponse, @@ -259,4 +290,4 @@ async def delete_open_type( "code": 0, "message": "删除成功", "data": None, - } \ No newline at end of file + } diff --git a/video-gen-api/app/services/material_consumption_service.py b/video-gen-api/app/services/material_consumption_service.py new file mode 100644 index 00000000..5fd3eb00 --- /dev/null +++ b/video-gen-api/app/services/material_consumption_service.py @@ -0,0 +1,160 @@ +from typing import Any, Optional, Tuple, List +from datetime import datetime + +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 + + +async def get_consumption_list( + db: AsyncSession, + page: int = 1, + page_size: int = 20, + advertiser_id: Optional[str] = None, + user_id: Optional[str] = None, + consume_date: Optional[List[str]] = None, + current_user: Optional[User] = None, +) -> Tuple[List[MaterialCost], int]: + offset = (page - 1) * page_size + + query = select(MaterialCost).where(MaterialCost.deleted_at.is_(None)) + + if current_user: + query = query.join( + UserOAuth, + MaterialCost.oauth_id == UserOAuth.id, + ).where( + UserOAuth.user_id == current_user.id, + UserOAuth.deleted_at.is_(None), + ) + + if user_id: + query = query.join( + UserOAuth, + MaterialCost.oauth_id == UserOAuth.id, + ).where(UserOAuth.user_id == user_id) + + if advertiser_id: + query = query.where(MaterialCost.advertiser_id == advertiser_id) + + if consume_date and len(consume_date) >= 2: + start_date_obj = datetime.strptime(consume_date[0], "%Y-%m-%d").date() + end_date_obj = datetime.strptime(consume_date[1], "%Y-%m-%d").date() + query = query.where(MaterialCost.consume_date >= start_date_obj) + 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)).where(MaterialCost.deleted_at.is_(None)) + + if current_user: + count_query = count_query.join( + UserOAuth, + MaterialCost.oauth_id == UserOAuth.id, + ).where( + UserOAuth.user_id == current_user.id, + UserOAuth.deleted_at.is_(None), + ) + + if user_id: + count_query = count_query.join( + UserOAuth, + MaterialCost.oauth_id == UserOAuth.id, + ).where(UserOAuth.user_id == user_id) + + if advertiser_id: + count_query = count_query.where(MaterialCost.advertiser_id == advertiser_id) + + if consume_date and len(consume_date) >= 2: + start_date_obj = datetime.strptime(consume_date[0], "%Y-%m-%d").date() + end_date_obj = datetime.strptime(consume_date[1], "%Y-%m-%d").date() + count_query = count_query.where(MaterialCost.consume_date >= start_date_obj) + count_query = count_query.where(MaterialCost.consume_date <= end_date_obj) + + total_result = await db.execute(count_query) + total = total_result.scalar_one() + + return consumptions, total + + +def format_consumption_response(consumptions: List[MaterialCost], include_oauth_id: bool = False) -> List[dict]: + result = [] + for consumption in consumptions: + item = { + "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, + } + if include_oauth_id: + item["oauth_id"] = consumption.oauth_id + result.append(item) + return result \ No newline at end of file