"""admin 广告收益报表:按 用户/日期/广告类型/应用/代码位 聚合 展示条数 / 收益 / 金币。 任意已登录 admin 可看(只读,不涉及资金操作)。聚合逻辑在 app/admin/repositories/ad_revenue.py。 """ from __future__ import annotations from datetime import date as _date from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query from app.admin.deps import AdminDb, get_current_admin from app.admin.repositories import ad_revenue from app.admin.schemas.ad_revenue import ( AdRevenueDaily, AdRevenueHourly, AdRevenueReportOut, AdRevenueRow, AdRevenueTypeStat, ) from app.core.rewards import cn_today router = APIRouter( prefix="/admin/api/ad-revenue-report", tags=["admin-ad-revenue-report"], dependencies=[Depends(get_current_admin)], ) # 区间最大跨度(天);超出拒绝,避免审计页一次拉过多天(逐日审计 + 大查询)拖垮接口。 _MAX_RANGE_DAYS = 92 def _parse_day(value: str | None, *, field: str, default: _date) -> _date: if value is None: return default try: return _date.fromisoformat(value) except ValueError as e: raise HTTPException(status_code=422, detail=f"{field} 需为 YYYY-MM-DD") from e @router.get("", response_model=AdRevenueReportOut, summary="广告收益报表(按 日期区间/用户/类型/应用/代码位 聚合)") def get_ad_revenue_report( db: AdminDb, date_from: Annotated[str | None, Query(description="起始日 北京时间 YYYY-MM-DD,默认今天")] = None, date_to: Annotated[str | None, Query(description="结束日 北京时间 YYYY-MM-DD,闭区间,默认=date_from")] = None, user_id: Annotated[int | None, Query(description="只看某用户;不传=全部用户")] = None, ad_type: Annotated[ str | None, Query(description="reward_video / feed / draw;不传=全部类型"), ] = None, feed_scene: Annotated[ str | None, Query( description="comparison(比价) / coupon(领券) / welfare(福利);不传=全部场景。" "全局筛选,同时影响明细 / 合计 / 趋势" ), ] = None, app_env: Annotated[ str | None, Query( description="prod(正式应用) / test(测试应用);不传=全部。" "建议正式收益报表选 prod,避免测试应用的假 eCPM 污染收益合计/平均" ), ] = None, granularity: Annotated[ str, Query(description="day=按天 / hour=按小时(北京时间);区间>1 天建议用 day") ] = "day", limit: Annotated[int, Query(ge=1, le=1000, description="每页条数(分页大小)")] = 500, offset: Annotated[int, Query(ge=0, description="分页偏移(已跳过的条数)=(页码-1)×每页条数")] = 0, sort: Annotated[ str, Query(description="排序:time=时间倒序(默认) / ecpm=按 eCPM 数值倒序") ] = "time", ) -> AdRevenueReportOut: today = cn_today() d_from = _parse_day(date_from, field="date_from", default=today) d_to = _parse_day(date_to, field="date_to", default=d_from) if d_to < d_from: raise HTTPException(status_code=422, detail="date_to 不能早于 date_from") if (d_to - d_from).days + 1 > _MAX_RANGE_DAYS: raise HTTPException(status_code=422, detail=f"区间最长 {_MAX_RANGE_DAYS} 天") result = ad_revenue.ad_revenue_report( db, date_from=d_from.isoformat(), date_to=d_to.isoformat(), user_id=user_id, ad_type=ad_type, feed_scene=feed_scene, app_env=app_env, granularity=granularity, limit=limit, offset=offset, sort=sort, ) return AdRevenueReportOut( date_from=d_from.isoformat(), date_to=d_to.isoformat(), daily=[AdRevenueDaily(**d) for d in result["daily"]], hourly=[AdRevenueHourly(**h) for h in result["hourly"]], type_stats={k: AdRevenueTypeStat(**v) for k, v in result["type_stats"].items()}, scene_stats={k: AdRevenueTypeStat(**v) for k, v in result["scene_stats"].items()}, dau=result["dau"], total=result["total"], truncated=result["truncated"], total_impressions=result["total_impressions"], total_revenue_yuan=result["total_revenue_yuan"], total_pangle_revenue_yuan=result["total_pangle_revenue_yuan"], total_pangle_api_revenue_yuan=result["total_pangle_api_revenue_yuan"], pangle_revenue_available=result["pangle_revenue_available"], total_expected_coin=result["total_expected_coin"], total_actual_coin=result["total_actual_coin"], mismatch_count=result["mismatch_count"], items=[AdRevenueRow(**r) for r in result["items"]], )