e38120ad49
收益报表(/admin/api/ad-revenue-report): - 明细改按时间倒序;新增 offset 真分页(limit 作每页大小、total 为全量),可翻页看当前筛选下全部数据,突破原 1000 条上限。 - 「场景」(feed_scene)下推后端做全局筛选,同时作用于明细/合计/daily·hourly 趋势(原为前端仅过滤明细)。 - 新增全量 hourly 序列,按小时趋势改用它,不再受分页截断影响。 修复信息流金币审计复算口径漂移(ad_audit): - 发奖侧 grant_feed_reward 早已是「一条广告=1份、LT 按账号累计条数(COUNT)」,但审计仍按 unit_count 逐份累加 + SUM(unit_count) 做 LT 基线,导致单条停留>20s(份数>1)时应发虚高、必然「✗ 不符」。 - 审计改为每条 granted 按 1 份复算、LT 基线用 COUNT,与发奖对齐;金币审计页与收益报表(复用同一复算)一并恢复正确。纯复算口径修正,不改实际发奖、不动钱。 文档同步更新 admin-ad-revenue-report.md。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #84 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
97 lines
4.0 KiB
Python
97 lines
4.0 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,
|
|
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,
|
|
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_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"]],
|
|
)
|