07c0b7502c
提现人工审核(提现不再即时打款): - models/wallet.py: WithdrawOrder 状态机改为 reviewing →(审核通过)→ pending → success/failed;reviewing →(驳回)→ rejected(已退款), 默认态 pending → reviewing(扣现金建单但不打款);新增 user_name 列(实名, 微信达额转账要求) - admin/routers/withdraw.py + admin/schemas/wallet.py: 运营后台审核接口(通过触发微信转账 / 驳回退款) - api/v1/wallet.py + repositories/wallet.py: 发起提现进 reviewing 态, 驳回退回现金并写 withdraw_refund - schemas/welfare.py: 提现出参增 reviewing/rejected 状态 + fail_reason 看广告每日时长限流(防刷主闸 + 次数兜底): - 新增 models/ad_watch_log.py: 按 (user_id, watch_date) 聚合当日观看秒数 - 新增 repositories/ad_watch.py: watched_seconds_today / add_watch_seconds(夹 [0, MAX_SINGLE_WATCH_SECONDS]) - api/v1/ad.py: 新增 POST /ad/watch-report(JWT 取 user, 落时长返当日累计); /ad/reward-status 增 watched_seconds_today / limit / remaining - schemas/ad.py: WatchReportIn/Out - core/rewards.py: 新增 DAILY_AD_WATCH_SECONDS_LIMIT=50 分钟(主闸)+ MAX_SINGLE_WATCH_SECONDS=120; DAILY_AD_REWARD_LIMIT 20→200 改作次数兜底(防前端少报时长绕过时长闸, 又不误伤看短广告的正常用户) - repositories/ad_reward.py + models/__init__.py: 接入新表与时长闸 alembic: withdraw_review_and_ad_watch 迁移(withdraw_order.user_name 列 + ad_watch_log 表) tests: 补 test_ad_reward / test_withdraw Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: pure <pure@192.168.0.104> Reviewed-on: #15
95 lines
4.4 KiB
Python
95 lines
4.4 KiB
Python
"""看激励视频发奖相关 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"
|
|
)
|