"""看广告金币审计 schemas。 只读对账视图:把"看视频"(ad_reward_record)和"信息流"(ad_feed_reward_record)两类发奖记录, 用与正式发奖相同的公式 [app.core.rewards.calculate_ad_reward_coin] 复算一遍 expected_coin, 和实际入账的 actual_coin 对比,核对金币公式是否生效。字段 snake_case、金额按金币整数。 """ from __future__ import annotations from datetime import datetime from pydantic import BaseModel, Field class AdCoinAuditRow(BaseModel): """单条发奖记录的复算明细。""" scene: str = Field(..., description="reward_video(看视频) / feed(信息流)") record_id: int = Field(..., description="对应记录表主键") user_id: int created_at: datetime status: str = Field(..., description="granted / capped / ecpm_missing") ecpm: str | None = Field(None, description="本次采用的 eCPM 原始值(分/千次展示,SDK getEcpm 原值)") ecpm_factor: float | None = Field(None, description="因子1(eCPM 档)") 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="复算与实发是否一致(capped/ecpm_missing 校验是否确为 0)") class AdCoinFormulaOut(BaseModel): """当前金币公式参数(给前端展示规则参照)。""" description: str = Field( "eCPM元 = getEcpm分 ÷ 100;单份金币 = round(eCPM元 ÷ 1000 × 因子1 × 因子2 × coin_per_yuan);" "因子1 按 eCPM元 判档(阈值 100/200/400 元)", description="公式说明", ) coin_per_yuan: int = Field(..., description="金币:元 汇率") ecpm_unit: str = Field("分/千次展示(SDK getEcpm 原值)", description="eCPM 口径") feed_unit_seconds: int = Field(..., description="信息流每多少秒折 1 份") # [(因子值, 区间下限, 区间上限或 null)] ecpm_factor_tiers: list[tuple[float, int, int | None]] = Field(..., description="因子1 档位表") lt_factor_tiers: list[tuple[float, int, int | None]] = Field(..., description="因子2 LT 档位表") class AdCoinAuditOut(BaseModel): """审计响应:公式参照 + 全量统计 + 明细。""" date: str = Field(..., description="审计日期(北京时间 YYYY-MM-DD)") formula: AdCoinFormulaOut total: int = Field(..., description="该筛选下复算总条数(全量,不受 limit/only_mismatch 影响)") mismatch_count: int = Field( ..., description="全量不一致条数(=0 说明公式全部生效;在截断前统计,可信)" ) truncated: bool = Field( ..., description="展示集是否被 limit 截断(true=还有未返回的明细,请缩小范围或调大 limit)" ) items: list[AdCoinAuditRow] = Field(..., description="展示明细;only_mismatch=true 时只含 ✗ 行")