81639ed388
广告收益报表两处隐患修复(admin/repositories/ad_revenue.py + routers/ad_revenue.py): - 收益钳顶:单次展示收益原用裸 parse_ecpm_yuan、未钳上限;发奖侧 calculate_ad_reward_coin 已钳 AD_ECPM_MAX_FEN(¥500 CPM)。异常/伪造天价 eCPM 会让报表预估收益虚高任意大。改为 min(parse_ecpm_yuan, AD_ECPM_MAX_FEN/100)/1000,与发奖同口径。 - app_env 过滤:报表聚合原无 app_env 过滤,测试应用假 eCPM(如 ¥678 CPM)会进正式收益合计/平均。 新增 app_env 参数(repository + router Query),显式传 prod/test 才过滤;默认仍全部(不擅自改成 默认排除 test:本地 dev 库多为 test 会空、且属产品口径)。穿山甲后台收益列暂未联动此过滤。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #93 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
107 lines
4.5 KiB
Python
107 lines
4.5 KiB
Python
"""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()},
|
|
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"]],
|
|
)
|