"""Pydantic 请求/响应模型""" from datetime import datetime, timezone from typing import Optional, Any from pydantic import BaseModel, Field, ConfigDict, model_serializer from .huya_defaults import ( HUYA_DEFAULT_BIND_ACT_ID, HUYA_DEFAULT_OUTER_ACT_ID, HUYA_DEFAULT_PAY_CHANNEL, HUYA_DEFAULT_ROOM_PID, HUYA_DEFAULT_SID, ) def _ensure_tz(dt: Optional[datetime]) -> Optional[datetime]: """确保 datetime 带有 UTC 时区信息,无时区的视为 UTC。""" if dt is None: return None if dt.tzinfo is None: return dt.replace(tzinfo=timezone.utc) return dt # ---- 认证 ---- class LoginRequest(BaseModel): username: str password: str class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" role: str username: str permissions: list[str] class UserInfo(BaseModel): id: int username: str role: str is_active: bool remark: str = "" created_at: Optional[datetime] = None permissions: list[str] = [] custom_permissions: Optional[list[str]] = None model_config = ConfigDict(from_attributes=True) @model_serializer def _serialize(self) -> dict[str, Any]: return { "id": self.id, "username": self.username, "role": self.role, "is_active": self.is_active, "remark": self.remark, "created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "permissions": self.permissions, "custom_permissions": self.custom_permissions, } # ---- 用户管理 ---- class UserCreate(BaseModel): username: str = Field(..., min_length=2, max_length=64) password: str = Field(..., min_length=6, max_length=128) role: str = Field("support", pattern="^(super_admin|operation|support)$") remark: str = "" class UserUpdate(BaseModel): password: Optional[str] = None role: Optional[str] = None is_active: Optional[bool] = None remark: Optional[str] = None custom_permissions: Optional[list[str]] = None # ---- 账号 ---- class AccountImport(BaseModel): """批量导入,文本格式:用户名|密码|邮箱|邮箱密码""" text: str class AccountAssign(BaseModel): assigned_to: Optional[int] = None class BatchAssign(BaseModel): account_ids: list[int] assigned_to: Optional[int] = None # None=取消分配 class AccountTag(BaseModel): tag: Optional[str] = None account_ids: Optional[list[int]] = None class AccountOut(BaseModel): id: int username: str # 敏感字段根据角色决定是否返回 password: Optional[str] = None email: Optional[str] = None email_password: Optional[str] = None tag: str = "" assigned_to: Optional[int] = None assigned_username: Optional[str] = None remark: str = "" created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) @model_serializer def _serialize(self) -> dict[str, Any]: return { "id": self.id, "username": self.username, "password": self.password, "email": self.email, "email_password": self.email_password, "tag": self.tag, "assigned_to": self.assigned_to, "assigned_username": self.assigned_username, "remark": self.remark, "created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, } # ---- 登录任务 ---- class LoginBatchRequest(BaseModel): account_ids: list[int] max_login_retries: int = 0 # 登录整体重试次数,0=无限重试直到成功(后端兜底 20 次) max_total_time: float = 300 # 单账号登录总时长上限(秒),0=不限制 concurrency: int = 3 # 并发数,1-10 api_strategy: str = "wgapi" # 接口策略: wgapi(新版)或 iframe(旧版备选) mode: str = Field("login", pattern="^(login|check)$") # login=登录取CK,check=账号状态检测 class LoginTaskOut(BaseModel): id: int batch_id: str account_id: int account_username: str = "" status: str cookie: str = "" message: str = "" created_by: int created_at: Optional[datetime] = None finished_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) @model_serializer def _serialize(self) -> dict[str, Any]: return { "id": self.id, "batch_id": self.batch_id, "account_id": self.account_id, "account_username": self.account_username, "status": self.status, "cookie": self.cookie, "message": self.message, "created_by": self.created_by, "created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "finished_at": _ensure_tz(self.finished_at).isoformat() if self.finished_at else None, } # ---- 账号检测 ---- class AccountCheckBatchRequest(BaseModel): text: str = Field(..., min_length=1) concurrency: int = Field(3, ge=1, le=10) max_login_retries: int = Field(0, ge=0, le=50) max_total_time: float = Field(300, ge=0, le=3600) use_proxy: bool = False class AccountCheckItemOut(BaseModel): line: int username: str email: str status: str message: str started_at: Optional[datetime] = None finished_at: Optional[datetime] = None class AccountCheckBatchOut(BaseModel): batch_id: str status: str message: str created_by: int concurrency: int max_login_retries: int max_total_time: float use_proxy: bool total: int finished_count: int running_count: int status_counts: dict[str, int] created_at: Optional[datetime] = None started_at: Optional[datetime] = None finished_at: Optional[datetime] = None items: list[AccountCheckItemOut] # ---- 虎牙 ---- class HuyaCookieImport(BaseModel): """批量导入虎牙 Cookie。支持纯 CK 或 账号----密码----CK。""" text: str tag: str = "" class HuyaPasswordLoginRequest(BaseModel): """虎牙账号密码登录并保存 Cookie。""" username: str = Field(..., min_length=1, max_length=128) password: str = Field(..., min_length=1, max_length=128) tag: str = "" cookie: str = "" class HuyaSmsCodeRequest(BaseModel): """发送虎牙短信验证码。""" phone: str = Field(..., min_length=5, max_length=32) cookie: str = "" class HuyaSmsLoginRequest(BaseModel): """提交虎牙短信验证码并保存 Cookie。""" authcode: str = Field(..., min_length=4, max_length=8) state: str = Field(..., min_length=1) phone: str = "" tag: str = "" class HuyaAutoRegisterRequest(BaseModel): """虎牙自动注册批次。每行格式:手机号----短信查询URL。""" text: str = Field(..., min_length=1) tag: str = "" concurrency: int = Field(1, ge=1, le=5) wait_seconds: float = Field(180, ge=15, le=600) poll_interval: float = Field(5, ge=1, le=30) password_prefix: str = Field("hy", max_length=8) fixed_password: str = Field("", max_length=64) use_proxy: bool = False class HuyaAutoRegisterItemOut(BaseModel): line: int phone: str provider: str sms_url: str = "" status: str message: str code: str = "" change_code: str = "" attempts: int = 0 change_attempts: int = 0 account_id: Optional[int] = None username: str = "" uid: str = "" password: str = "" password_changed: bool = False cookie: str = "" cookie_preview: str = "" started_at: Optional[datetime] = None finished_at: Optional[datetime] = None class HuyaAutoRegisterBatchOut(BaseModel): batch_id: str status: str message: str tag: str = "" created_by: int concurrency: int wait_seconds: float poll_interval: float password_prefix: str = "hy" use_proxy: bool = False total: int success_count: int failed_count: int stopped_count: int running_count: int created_at: Optional[datetime] = None started_at: Optional[datetime] = None finished_at: Optional[datetime] = None items: list[HuyaAutoRegisterItemOut] class HuyaPasswordAccountImport(BaseModel): """导入虎牙账号密码,稍后再选择登录。""" text: str = Field(..., min_length=1) tag: str = "" class HuyaPasswordLoginSelectedRequest(BaseModel): """选择已导入的虎牙账号执行密码登录。""" account_ids: list[int] class HuyaAccountOut(BaseModel): id: int uid: str = "" yyuid: str = "" username: str = "" has_password: bool = False nickname: str = "" cookie: str = "" cookie_preview: str = "" tag: str = "" remark: str = "" status: str = "" points: Optional[int] = None game_name: str = "" game_channel: str = "" game_phone: str = "" assigned_to: Optional[int] = None assigned_username: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) @model_serializer def _serialize(self) -> dict[str, Any]: return { "id": self.id, "uid": self.uid, "yyuid": self.yyuid, "username": self.username, "has_password": self.has_password, "nickname": self.nickname, "cookie": self.cookie, "cookie_preview": self.cookie_preview, "tag": self.tag, "remark": self.remark, "status": self.status, "points": self.points, "game_name": self.game_name, "game_channel": self.game_channel, "game_phone": self.game_phone, "assigned_to": self.assigned_to, "assigned_username": self.assigned_username, "created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, } class HuyaConfigOut(BaseModel): room_pid: str = HUYA_DEFAULT_ROOM_PID sid: str = HUYA_DEFAULT_SID outer_act_id: str = HUYA_DEFAULT_OUTER_ACT_ID bind_act_id: str = HUYA_DEFAULT_BIND_ACT_ID pay_channel: str = HUYA_DEFAULT_PAY_CHANNEL updated_at: Optional[datetime] = None @model_serializer def _serialize(self) -> dict[str, Any]: return { "room_pid": self.room_pid, "sid": self.sid, "outer_act_id": self.outer_act_id, "bind_act_id": self.bind_act_id, "pay_channel": self.pay_channel, "updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, } class HuyaConfigUpdate(BaseModel): room_pid: Optional[str] = None sid: Optional[str] = None outer_act_id: Optional[str] = None bind_act_id: Optional[str] = None pay_channel: Optional[str] = None class HuyaTaskBatchRequest(BaseModel): account_ids: list[int] task_type: str concurrency: int = 3 payload: dict[str, Any] = Field(default_factory=dict) class HuyaTaskOut(BaseModel): id: int batch_id: str account_id: int account_uid: str = "" account_nickname: str = "" task_type: str status: str message: str = "" result: Optional[dict[str, Any]] = None created_by: int created_at: Optional[datetime] = None finished_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) @model_serializer def _serialize(self) -> dict[str, Any]: return { "id": self.id, "batch_id": self.batch_id, "account_id": self.account_id, "account_uid": self.account_uid, "account_nickname": self.account_nickname, "task_type": self.task_type, "status": self.status, "message": self.message, "result": self.result, "created_by": self.created_by, "created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "finished_at": _ensure_tz(self.finished_at).isoformat() if self.finished_at else None, } class HuyaGoodsOut(BaseModel): id: int product_id: str name: str = "" price: Optional[int] = None remain_text: str = "" raw: Optional[dict[str, Any]] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) @model_serializer def _serialize(self) -> dict[str, Any]: return { "id": self.id, "product_id": self.product_id, "name": self.name, "price": self.price, "remain_text": self.remain_text, "raw": self.raw, "updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, } class HuyaRechargeGoodsOut(BaseModel): id: int spu_id: str sku_id: str = "" name: str = "" price: Optional[int] = None stock: Optional[int] = None buy_limit: Optional[int] = None icon: str = "" description: str = "" task_id: str = "" task_name: str = "" raw: Optional[dict[str, Any]] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) @model_serializer def _serialize(self) -> dict[str, Any]: return { "id": self.id, "spu_id": self.spu_id, "sku_id": self.sku_id, "name": self.name, "price": self.price, "stock": self.stock, "buy_limit": self.buy_limit, "icon": self.icon, "description": self.description, "task_id": self.task_id, "task_name": self.task_name, "raw": self.raw, "updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, } # ---- 代理配置 ---- class ProxyConfigOut(BaseModel): enabled: bool = False api_url: str = "" http: str = "" https: str = "" whitelist_enabled: bool = False whitelist_platform: str = "xiequ" whitelist_credentials: Optional[dict] = None # 旧字段保留(向后兼容) whitelist_uid: str = "" whitelist_ukey: str = "" class ProxyConfigUpdate(BaseModel): enabled: Optional[bool] = None api_url: Optional[str] = None http: Optional[str] = None https: Optional[str] = None whitelist_enabled: Optional[bool] = None whitelist_platform: Optional[str] = "xiequ" whitelist_credentials: Optional[dict] = None # 旧字段保留(向后兼容) whitelist_uid: Optional[str] = None whitelist_ukey: Optional[str] = None # ---- 代理平台元信息 ---- class PlatformFieldDef(BaseModel): """平台凭据字段定义。""" key: str label: str placeholder: str = "" class PlatformInfo(BaseModel): """平台元信息。""" name: str label: str credential_fields: list[PlatformFieldDef] # ---- 通用 ---- class AppInfo(BaseModel): version: str class MessageResponse(BaseModel): message: str success: bool = True