"""admin 用户管理 schemas。""" from __future__ import annotations from datetime import datetime from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator class AdminUserListItem(BaseModel): model_config = ConfigDict(from_attributes=True) id: int phone: str nickname: str | None = None register_channel: str status: str debug_trace_enabled: bool = False wechat_openid: str | None = None created_at: datetime last_login_at: datetime class AdminUserOverview(BaseModel): """用户 360 概览:基础资料 + 钱包余额 + 各项 count(历史明细走各自分页接口)。""" model_config = ConfigDict(from_attributes=True) user: AdminUserListItem coin_balance: int cash_balance_cents: int total_coin_earned: int comparison_total: int comparison_success: int withdraw_total: int withdraw_success_cents: int feedback_total: int class UserRewardStats(BaseModel): """提现详情抽屉「用户统计区」:按时间窗口算的提现 + 看广告行为统计。 窗口由 date_from/date_to 决定(都不传 = 注册至今 = 全量);现金余额除外——它是当前快照。 各「*_cash_cents」是把该来源累计发放金币折算成可提现现金(分):100 金币 = 1 分(COIN_PER_YUAN=10000)。 eCPM 单位沿用穿山甲原值「分/千次」。 """ withdraw_success_cents: int # 累计提现(窗口内 success 金额) cash_balance_cents: int # 现金余额(当前快照,不随窗口) withdraw_total: int # 提现总次数(窗口内全部状态) traditional_task_cash_cents: int # 传统任务提现(非广告非人工的金币折现) reward_video_count: int # 累计激励视频数(granted 条数) reward_video_avg_ecpm: float # 平均激励视频 eCPM(分/千次) reward_video_cash_cents: int # 激励视频提现(金币折现) feed_count: int # 累计信息流广告数(granted 份数,unit_count 累加) feed_avg_ecpm: float # 平均信息流广告 eCPM(分/千次) feed_cash_cents: int # 信息流广告提现(金币折现) class UserCoinRecord(BaseModel): """金币发放记录(提现详情底部表)。source 见 source_label;非广告来源 ecpm 为 None。""" source: str # reward_video / signin_boost / feed / signin source_label: str # 激励视频 / 签到膨胀 / 信息流广告 / 签到 created_at: datetime ecpm: str | None = None # 原始 eCPM(分/千次),非广告为 None coin: int # 发放金币数 def _strip_reason(v: str) -> str: # min_length=1 放过纯空白(" "),trim 后再校验非空,避免审计记到空原因 if not v.strip(): raise ValueError("操作原因不能为空") return v.strip() class GrantCoinsRequest(BaseModel): mode: Literal["delta", "set"] = Field( "delta", description="delta=增减(amount 为变动量) / set=设为(amount 为目标值,须≥0)" ) amount: int = Field( ..., description="delta 模式:金币变动(正=增加,负=扣减,不可为 0);set 模式:目标金币值(须≥0)", ) reason: str = Field(..., min_length=1, max_length=128, description="操作原因(必填,入审计)") _v_reason = field_validator("reason")(_strip_reason) class GrantCashRequest(BaseModel): mode: Literal["delta", "set"] = Field( "delta", description="delta=增减(amount_cents 为变动量) / set=设为(amount_cents 为目标值,须≥0)" ) amount_cents: int = Field( ..., description="delta 模式:现金变动(分,正=增加,负=扣减,不可为 0);set 模式:目标现金值(分,须≥0)", ) reason: str = Field(..., min_length=1, max_length=128, description="操作原因(必填,入审计)") _v_reason = field_validator("reason")(_strip_reason) class SetUserStatusRequest(BaseModel): status: Literal["active", "disabled"] = Field( ..., description="active=解封 / disabled=封禁(注销 deleted 不走此接口)" ) class SetDebugTraceRequest(BaseModel): enabled: bool = Field(..., description="是否给该用户开「复制调试链接」权限")