128db7fb86
报表主表从「按 用户×类型×应用×代码位 聚合」改成「逐条广告事件」(每次广告一行): - 激励视频:展示(ad_ecpm)与发奖(ad_reward)按 ad_session_id 合并成一行,直接给出 eCPM/收益 + 状态/应发/实发/一致;展开看该条金币复算因子 - 信息流:轮播每条展示各一行;整场发奖(client_event_id 与展示 impressionId 对不上)单独成行 - 纯展示行不计对账(matched 恒 true);有展示无发奖 / 有发奖无展示各自成行 - 每行补 user_phone(批量查 User.phone,完整不脱敏,与用户/钱包/比价记录页一致) - 合计与对账在全量上统计、不受 limit 影响;event_key 作前端稳定 rowKey ad_audit.audit_rows 顺带补返回 ad_session_id(供展示↔发奖按会话合并)。 真实库验证:逐条输出正确、合计交叉核对一致(展示条数=ecpm行数、实发=库实发)、schema 校验通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #72 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
97 lines
6.4 KiB
Python
97 lines
6.4 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):
|
||
"""按日期汇总的一天(供前端按天趋势图;全量,不受 limit 影响)。"""
|
||
|
||
date: str = Field(..., description="北京时间 YYYY-MM-DD")
|
||
impressions: int = Field(..., description="当天展示条数合计")
|
||
revenue_yuan: float = Field(..., description="当天预估收益合计(元)")
|
||
expected_coin: int = Field(..., description="当天应发金币合计")
|
||
actual_coin: int = 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 信息流)")
|
||
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="按日期汇总序列(全量,供按天趋势图)")
|
||
total: int = Field(..., description="广告事件总数(全量,不受 limit 影响)")
|
||
truncated: bool = Field(..., description="明细是否被 limit 截断")
|
||
total_impressions: int = Field(..., description="全量展示条数合计")
|
||
total_revenue_yuan: float = Field(..., description="全量收益合计(元)")
|
||
total_expected_coin: int = Field(..., description="全量应发金币合计")
|
||
total_actual_coin: int = Field(..., description="全量实发金币合计")
|
||
mismatch_count: int = Field(..., description="应发≠实发的发奖条数(=0 说明全部按公式发放)")
|
||
items: list[AdRevenueRow] = Field(..., description="逐条广告事件(按 日期→用户→时间 排序)")
|