b622f76a02
- 新增穿山甲 GroMore「聚合数据报告 API」对接,按天 T+1 拉取后台 revenue(预估)与 api_revenue(收益API,更接近结算),在广告收益报表大盘 + 按天趋势级展示,与客户端自报 eCPM 折算的预估并列对照;逐条广告事件行不动(仍是客户端预估)。 - 链路:integrations/pangle_report.py(MD5 签名,与官方文档两个测试向量逐字节一致)→ scripts/sync_pangle_revenue.py(拉昨天/--days 回补)→ 新表 ad_pangle_daily_revenue (repositories/ad_pangle_revenue.py:upsert + 按日聚合)→ admin/repositories/ad_revenue.py 汇总,新增 total_pangle_revenue_yuan / total_pangle_api_revenue_yuan / daily[].pangle_*。 - 穿山甲无用户/类型/场景维度:仅全量视图(未按 user/ad_type/feed_scene 过滤)给值,否则置 None;join key 用 ad_unit_id(=客户端配的 104xxx),非 code_id。 - 新增配置 PANGLE_REPORT_USER_ID/ROLE_ID/SECURITY_KEY(≠发奖 m-key)+ site_id→应用映射; 含单测(签名向量+分页解析);已真连穿山甲验证。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #92 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
151 lines
9.3 KiB
Python
151 lines
9.3 KiB
Python
"""广告收益报表 schemas。
|
||
|
||
**逐条广告事件**只读报表:每行一次广告(激励视频展示+发奖按会话合并;信息流展示/发奖各自成行),
|
||
含 展示条数、收益(元)、应发/实发金币、对账。字段 snake_case;收益按元(float),金币按整数。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
class AdRevenueImpression(BaseModel):
|
||
"""聚合行下钻的单条**展示**明细(每次广告展示一条,展开该组时展示)。"""
|
||
|
||
id: int = Field(..., description="ad_ecpm_record 主键")
|
||
created_at: datetime
|
||
ecpm: str = Field(..., description="本次展示 eCPM 原始值(分/千次展示)")
|
||
revenue_yuan: float = Field(..., description="本次展示预估收益(元)= eCPM元 ÷ 1000")
|
||
adn: str | None = Field(None, description="实际填充 ADN 子渠道(pangle/gdt…)")
|
||
slot_id: str | None = Field(None, description="底层 mediation rit(非我们配置的广告位 ID)")
|
||
|
||
|
||
class AdRevenueRecord(BaseModel):
|
||
"""聚合行下钻的单条发奖复算明细(与金币审计同源,展开该组时展示)。"""
|
||
|
||
record_id: int
|
||
created_at: datetime
|
||
status: str = Field(..., description="granted / capped / ecpm_missing")
|
||
ecpm: str | None = Field(None, description="本次采用的 eCPM 原始值(分/千次展示)")
|
||
ecpm_factor: float | None = Field(None, description="因子1(eCPM 档);非 granted 为空")
|
||
units: int = Field(..., description="折算份数:激励视频恒 1;信息流 = 满 10 秒份数")
|
||
lt_index_start: int | None = Field(None, description="本条占用「账号累计第几份」的起")
|
||
lt_index_end: int | None = Field(None, description="本条占用「账号累计第几份」的止;激励视频 = 起")
|
||
lt_factor_start: float | None = Field(None, description="因子2(LT)起值")
|
||
lt_factor_end: float | None = Field(None, description="因子2(LT)止值;激励视频 = 起")
|
||
expected_coin: int = Field(..., description="按公式复算应发金币")
|
||
actual_coin: int = Field(..., description="实际入账金币")
|
||
matched: bool = Field(..., description="复算与实发是否一致")
|
||
|
||
|
||
class AdRevenueDaily(BaseModel):
|
||
"""按日期汇总的一天(供前端按天趋势图;全量,不受分页影响)。"""
|
||
|
||
date: str = Field(..., description="北京时间 YYYY-MM-DD")
|
||
impressions: int = Field(..., description="当天展示条数合计")
|
||
revenue_yuan: float = Field(..., description="当天客户端预估收益合计(元;eCPM 折算)")
|
||
pangle_revenue_yuan: float | None = Field(
|
||
None, description="当天穿山甲后台预估收益(元;GroMore revenue);非全量视图/无数据为空"
|
||
)
|
||
pangle_api_revenue_yuan: float | None = Field(
|
||
None, description="当天穿山甲收益Api(元;GroMore api_revenue,更接近结算);未配/当天/无数据为空"
|
||
)
|
||
expected_coin: int = Field(..., description="当天应发金币合计")
|
||
actual_coin: int = Field(..., description="当天实发金币合计")
|
||
|
||
|
||
class AdRevenueHourly(BaseModel):
|
||
"""按北京小时(0–23)汇总的一小时(供前端按小时趋势图;全量,不受分页影响,单日 granularity=hour 时非空)。"""
|
||
|
||
hour: int = Field(..., description="北京时间小时 0–23")
|
||
impressions: int = Field(..., description="该小时展示条数合计")
|
||
revenue_yuan: float = Field(..., description="该小时预估收益合计(元)")
|
||
expected_coin: int = Field(..., description="该小时应发金币合计")
|
||
actual_coin: int = Field(..., description="该小时实发金币合计")
|
||
|
||
|
||
class AdRevenueTypeStat(BaseModel):
|
||
"""按广告类型(ad_type)的小计:展示条数 + 预估收益(eCPM 由前端用 收益÷展示×1000 算)。"""
|
||
|
||
impressions: int = Field(..., description="该类型展示条数合计")
|
||
revenue_yuan: float = Field(..., description="该类型预估收益合计(元)")
|
||
|
||
|
||
class AdRevenueRow(BaseModel):
|
||
"""一次广告事件(逐条一行):激励视频展示与发奖按 ad_session_id 合并;信息流展示 / 发奖各自成行。"""
|
||
|
||
event_key: str = Field(..., description="该事件稳定唯一键(imp-{ecpm_id} / rwd-{reward_id});前端 rowKey 用")
|
||
report_date: str = Field(..., description="该事件所属日期(北京时间 YYYY-MM-DD)")
|
||
user_id: int
|
||
user_phone: str | None = Field(None, description="用户手机号(admin 展示用,完整;用户已删 / 查不到为空)")
|
||
ad_type: str = Field(..., description="reward_video(激励视频) / feed(信息流) / draw(Draw 信息流);历史 NULL 视为 feed")
|
||
feed_scene: str | None = Field(
|
||
None,
|
||
description="点位场景:comparison(比价) / coupon(领券) / welfare(福利);供区分比价/领券 Draw 收益;"
|
||
"激励视频与旧数据为空",
|
||
)
|
||
app_env: str | None = Field(None, description="我们的应用:prod(傻瓜比价正式) / test(测试应用);旧数据为空")
|
||
our_code_id: str | None = Field(None, description="我们后台配置的代码位 ID(104xxx);旧数据为空")
|
||
hour: int | None = Field(None, description="北京时间小时 0–23(granularity=hour 时有值;按天为 null)")
|
||
created_at: datetime = Field(..., description="事件时间(有展示=展示时间,纯发奖=发奖时间)")
|
||
# ── 展示侧 ──
|
||
has_impression: bool = Field(..., description="是否有广告展示(信息流逐条展示=True,纯发奖行=False)")
|
||
impressions: int = Field(..., description="本行展示条数:有展示=1 / 纯发奖=0(供日汇总、趋势图复用)")
|
||
ecpm: str | None = Field(None, description="eCPM 原始值(分/千次);展示行取展示值,纯发奖行取发奖采用值")
|
||
revenue_yuan: float = Field(..., description="本次展示预估收益(元)= eCPM元 ÷ 1000;纯发奖行=0")
|
||
adn: str | None = Field(None, description="实际填充 ADN 子渠道(pangle/gdt…);纯发奖行为空")
|
||
slot_id: str | None = Field(None, description="底层 mediation rit(非我们配置的广告位 ID);纯发奖行为空")
|
||
# ── 发奖侧 ──
|
||
has_reward: bool = Field(..., description="是否有发奖记录(激励视频合并行 / 信息流整场发奖行=True;纯展示=False)")
|
||
status: str | None = Field(None, description="发奖状态 granted/closed_early/too_short/…;纯展示为空")
|
||
expected_coin: int = Field(..., description="应发金币(公式复算,与金币审计同源);纯展示=0")
|
||
actual_coin: int = Field(..., description="实发金币(实际入账);纯展示=0")
|
||
matched: bool = Field(..., description="本条应发==实发;纯展示恒 True(不计对账)")
|
||
reward_detail: AdRevenueRecord | None = Field(
|
||
None,
|
||
description="发奖复算明细(eCPM/因子1/份数/LT/因子2/应发/实发/一致);点行展开下钻用,纯展示为空",
|
||
)
|
||
|
||
|
||
class AdRevenueReportOut(BaseModel):
|
||
"""报表响应:全量统计 + 按天趋势 + 聚合明细。"""
|
||
|
||
date_from: str = Field(..., description="报表起始日期(北京时间 YYYY-MM-DD)")
|
||
date_to: str = Field(..., description="报表结束日期(北京时间 YYYY-MM-DD,闭区间;单日时与 date_from 相同)")
|
||
daily: list[AdRevenueDaily] = Field(..., description="按日期汇总序列(全量,供按天趋势图)")
|
||
hourly: list[AdRevenueHourly] = Field(
|
||
default_factory=list,
|
||
description="按小时汇总序列(全量,供按小时趋势图;按天查询时为空)",
|
||
)
|
||
type_stats: dict[str, AdRevenueTypeStat] = Field(
|
||
default_factory=dict,
|
||
description="按广告类型(ad_type)小计 {ad_type: {impressions, revenue_yuan}};前端取 draw / reward_video 做分类大盘",
|
||
)
|
||
dau: int | None = Field(
|
||
None,
|
||
description="今日活跃用户数(复用大盘口径,last_login_at);**仅查询=今日单天时有值**,历史/多天为 null",
|
||
)
|
||
total: int = Field(..., description="广告事件总数(全量,不受分页影响;= 当前筛选下的分页总条数)")
|
||
truncated: bool = Field(..., description="当前页之后是否还有更多事件(len(events) > offset + limit)")
|
||
total_impressions: int = Field(..., description="全量展示条数合计")
|
||
total_revenue_yuan: float = Field(..., description="全量客户端预估收益合计(元;eCPM 折算)")
|
||
total_pangle_revenue_yuan: float | None = Field(
|
||
None,
|
||
description="全量穿山甲后台预估收益合计(元;GroMore revenue)。穿山甲无用户/类型/场景维度,"
|
||
"仅「全量视图」(未按 user_id/ad_type/feed_scene 过滤)时有值,否则为 null",
|
||
)
|
||
total_pangle_api_revenue_yuan: float | None = Field(
|
||
None,
|
||
description="全量穿山甲收益Api合计(元;GroMore api_revenue,各 ADN 回传、更接近结算);"
|
||
"未配 Reporting / 查当天 / 非全量视图 时为 null",
|
||
)
|
||
pangle_revenue_available: bool = Field(
|
||
False,
|
||
description="本次结果是否带穿山甲后台收益(=全量视图且已同步到数据)。false 时前端「穿山甲收益」显示「-」",
|
||
)
|
||
total_expected_coin: int = Field(..., description="全量应发金币合计")
|
||
total_actual_coin: int = Field(..., description="全量实发金币合计")
|
||
mismatch_count: int = Field(..., description="应发≠实发的发奖条数(=0 说明全部按公式发放)")
|
||
items: list[AdRevenueRow] = Field(..., description="逐条广告事件(按 日期→用户→时间 排序)")
|