"""看激励视频发奖相关 schemas。 约定同其余模块:字段 snake_case、金额/次数存整数。 """ from __future__ import annotations from datetime import datetime from pydantic import BaseModel, Field class PangleCallbackOut(BaseModel): """回给穿山甲 GroMore 的回调响应。 GroMore 服务端激励回调规范(supportcenter/26240)要求响应体为 `{"is_verify": bool, "reason": int}`: - `is_verify=true` + `reason=0`:校验通过、发放奖励(发奖成功 或 当日已达上限均算"已处理")。 - `is_verify=false`:不发放,`reason` 带错误码透传客户端 SDK(见 ad.py 的 REASON_*)。 验签失败由 API 层直接返 403,不走这里。""" is_verify: bool = True reason: int = 0 class AdRewardStatusOut(BaseModel): """客户端查今日看广告发奖进度(福利页"看视频赚金币"用)。""" used_today: int = Field(..., description="今日已成功发奖次数") daily_limit: int = Field(..., description="每日发奖次数上限") remaining: int = Field(..., description="今日剩余可领次数") coin_per_ad: int = Field(..., description="看完一个激励视频发的金币") round_count: int = Field( ..., description="本轮(3 次一组)已看次数,0..N-1。客户端可由它判断'刚刚看完一轮'" ) cooldown_until: datetime | None = Field( None, description="本轮看完后 10 分钟冷却结束时间(UTC ISO),null 表示不在冷却。" "冷却是 UX 约束,后端发奖不受影响,客户端 CTA 据此显示 MM:SS 倒计时且不可点", ) # ===== 看广告每日总时长(50 分钟防刷主闸):客户端据此展示"今日已看 X/50 分钟"、满则不给看 ===== watched_seconds_today: int = Field(0, description="今日已观看激励视频总秒数(前端累计上报)") watch_seconds_limit: int = Field(0, description="每日观看总时长上限(秒,默认 3000=50 分钟)") watch_seconds_remaining: int = Field(0, description="今日剩余可观看秒数(<=0 时客户端不应再展示广告)") class EcpmReportIn(BaseModel): """客户端上报一次广告展示的 eCPM(内部收益统计/对账)。 user_id 不在 body 里——由 JWT 取(Bearer),防伪造。ecpm 原样上报字符串(单位待确认)。 """ ad_type: str = Field(..., description="广告类型:reward_video(激励视频) / draw(Draw 信息流) 等") ecpm: str = Field(..., description="穿山甲 getShowEcpm().getEcpm() 原始字符串,单位待确认,原样上报") adn: str | None = Field(None, description="实际投放 ADN(getSdkName),如 pangle") slot_id: str | None = Field(None, description="实际展示代码位(底层 mediation rit)") class EcpmReportOut(BaseModel): """eCPM 上报结果。best-effort,落库即 ok。""" ok: bool = True class WatchReportIn(BaseModel): """客户端上报一次激励视频的实际观看时长(onAdClose 时,onAdShow→onAdClose 墙钟秒数)。 user_id 由 JWT 取(Bearer),不在 body。seconds 服务端会夹 [0, MAX_SINGLE_WATCH_SECONDS]。 """ seconds: int = Field(..., ge=0, description="本次观看秒数") class WatchReportOut(BaseModel): """上报后返回当日累计 + 剩余,客户端即时更新"今日还能看多久"。""" watched_seconds_today: int = Field(..., description="今日累计观看秒数") watch_seconds_limit: int = Field(..., description="每日上限(秒)") watch_seconds_remaining: int = Field(..., description="今日剩余可观看秒数") class TestGrantOut(BaseModel): """[仅本地联调]模拟发奖结果。带上今日进度,客户端可直接据此刷新展示。""" granted: bool = Field(..., description="本次是否真的发了金币(达每日上限则 False)") status: str = Field(..., description="granted / capped") coin: int = Field(..., description="本次发放金币(capped 时为 0)") used_today: int = Field(..., description="今日已成功发奖次数") daily_limit: int = Field(..., description="每日发奖次数上限") remaining: int = Field(..., description="今日剩余可领次数") coin_per_ad: int = Field(..., description="看完一个激励视频发的金币") round_count: int = Field(..., description="本轮已看次数,详见 AdRewardStatusOut") cooldown_until: datetime | None = Field( None, description="本轮冷却结束时间(UTC),详见 AdRewardStatusOut" )