32f300b5a2
- GrantCashRequest 加 account(coin_cash / invite_cash,默认 coin_cash 兼容旧调用) - 调现金接口按 account 分支:邀请账户走 grant_invite_cash,余额读取/扣负保护/审计 detail 都按对应账户走;两本账物理隔离不可串 - 用户 360 overview 返回 invite_cash_balance_cents(admin 调现金弹窗展示邀请余额) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #95 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
"""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
|
|
invite_cash_balance_cents: int # 邀请奖励金余额(与 cash_balance_cents 物理隔离)
|
|
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):
|
|
# 目标账户:金币兑换的现金(cash_balance_cents)与邀请奖励金(invite_cash_balance_cents)物理隔离,
|
|
# 各调各的、不可串。默认 coin_cash 兼容旧调用。
|
|
account: Literal["coin_cash", "invite_cash"] = Field(
|
|
"coin_cash", description="目标账户:coin_cash=金币兑现金账户 / invite_cash=邀请奖励金账户"
|
|
)
|
|
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="是否给该用户开「复制调试链接」权限")
|