"""广告收益报表 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") row_revenue_yuan: float | None = Field( None, description="主表逐行展示用的预估收益(元):一次比价/领券聚合行=该次发奖广告 eCPM 折算之和;" "其它行为空(前端回退取 revenue_yuan)。不进合计/趋势,避免与展示侧重复计", ) 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/应发/实发/一致);点行展开下钻用,纯展示为空", ) sub_rewards: list[AdRevenueRecord] = Field( default_factory=list, description="一次比价/领券聚合行的组内逐条发奖明细(同一整场 ad_session_id 的多条广告);" "点行展开渲染多行。激励视频/纯展示行为空(单条看 reward_detail)", ) sub_count: int = Field( 1, description="本行聚合的发奖条数:一次比价/领券=该次广告条数(≥1);激励视频/纯展示=1", ) 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 做分类大盘", ) scene_stats: dict[str, AdRevenueTypeStat] = Field( default_factory=dict, description="按信息流场景(feed_scene)小计 {comparison/coupon/welfare: {impressions, revenue_yuan}};" "全量统计(不受分页截断),供数据大盘「领券广告 / 比价广告」卡;feed_scene 为空的事件不计入", ) dau: int | None = Field( None, description="所选日期区间的去重活跃用户数(口径同数据大盘 period.users.active:登录 + 开始比价 + " "开始领券)。按 date_from~date_to 区间统计,含今日、近 7 天、近 30 天等任意区间;全局口径," "不随 user_id / ad_type / feed_scene / app_env 筛选变化", ) 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="逐条广告事件(按 日期→用户→时间 排序)")