2024 lines
63 KiB
Python
2024 lines
63 KiB
Python
"""Pydantic 请求/响应模型"""
|
||
|
||
from datetime import UTC, datetime
|
||
from typing import Any
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field, 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,
|
||
)
|
||
|
||
|
||
class YybTaskCreateRequest(BaseModel):
|
||
"""创建 YYB 任务;登录方式在下一步选择。"""
|
||
|
||
|
||
class YybLoginRequest(BaseModel):
|
||
provider: str = Field(..., pattern="^(qq|wechat)$")
|
||
timeout: int = Field(600, ge=60, le=1800)
|
||
|
||
|
||
class YybSelectionRequest(BaseModel):
|
||
platform: str = Field(..., pattern="^(android|ios)$")
|
||
points: int = Field(..., gt=0)
|
||
product_id: str = Field(..., min_length=1, max_length=128)
|
||
zone_id: str = Field(..., min_length=1, max_length=64)
|
||
zone_name: str = Field("", max_length=128)
|
||
role_id: str = Field(..., min_length=1, max_length=64)
|
||
role_name: str = Field(..., min_length=1, max_length=128)
|
||
|
||
|
||
def _ensure_tz(dt: datetime | None) -> datetime | None:
|
||
"""确保 datetime 带有 UTC 时区信息,无时区的视为 UTC。"""
|
||
if dt is None:
|
||
return None
|
||
if dt.tzinfo is None:
|
||
return dt.replace(tzinfo=UTC)
|
||
return dt
|
||
|
||
|
||
def _isoformat(dt: datetime | None) -> str | None:
|
||
normalized = _ensure_tz(dt)
|
||
return normalized.isoformat() if normalized is not None else None
|
||
|
||
|
||
# ---- 认证 ----
|
||
class LoginRequest(BaseModel):
|
||
username: str
|
||
password: str
|
||
|
||
|
||
class SmsCodeRequest(BaseModel):
|
||
username: str
|
||
password: str
|
||
|
||
|
||
class SmsCodeResponse(BaseModel):
|
||
sent: bool = True
|
||
phone_masked: str = Field(..., serialization_alias="phoneMasked")
|
||
expires_in_seconds: int = Field(..., serialization_alias="expiresInSeconds")
|
||
provider: str = "mock"
|
||
|
||
|
||
class LoginPrepareRequest(BaseModel):
|
||
username: str
|
||
password: str
|
||
sms_code: str = Field(..., alias="smsCode")
|
||
api_base: str | None = Field(None, alias="apiBase")
|
||
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
|
||
class LoginPrepareResponse(BaseModel):
|
||
challenge_id: str = Field(..., serialization_alias="challengeId")
|
||
challenge: str
|
||
expires_at: str = Field(..., serialization_alias="expiresAt")
|
||
poll_token: str = Field(..., serialization_alias="pollToken")
|
||
launch_url: str = Field(..., serialization_alias="launchUrl")
|
||
phone_masked: str = Field(..., serialization_alias="phoneMasked")
|
||
app: str = "live_hub"
|
||
|
||
|
||
class LoginCompleteRequest(BaseModel):
|
||
challenge_id: str = Field(..., alias="challengeId")
|
||
poll_token: str = Field(..., alias="pollToken")
|
||
device_ticket: str = Field(..., alias="deviceTicket")
|
||
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
|
||
class DeviceAuthorizeRequest(BaseModel):
|
||
app: str
|
||
challenge_id: str = Field(..., alias="challengeId")
|
||
challenge: str
|
||
device_id: str = Field(..., alias="deviceId")
|
||
public_key: str = Field(..., alias="publicKey")
|
||
signature: str
|
||
timestamp: int | str
|
||
client_version: str = Field(..., alias="clientVersion")
|
||
|
||
model_config = ConfigDict(populate_by_name=True)
|
||
|
||
|
||
class UserPhoneBindRequest(BaseModel):
|
||
phone: 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: datetime | None = None
|
||
permissions: list[str] = []
|
||
custom_permissions: list[str] | None = None
|
||
phone: str = ""
|
||
phone_masked: str = ""
|
||
phone_bound_at: datetime | None = None
|
||
device_id: str = ""
|
||
device_id_masked: str = ""
|
||
device_bound_at: datetime | None = None
|
||
device_last_used_at: datetime | None = 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": _isoformat(self.created_at),
|
||
"permissions": self.permissions,
|
||
"custom_permissions": self.custom_permissions,
|
||
"phone": self.phone,
|
||
"phone_masked": self.phone_masked,
|
||
"phone_bound_at": _isoformat(self.phone_bound_at),
|
||
"device_id": self.device_id,
|
||
"device_id_masked": self.device_id_masked,
|
||
"device_bound_at": _isoformat(self.device_bound_at),
|
||
"device_last_used_at": _isoformat(self.device_last_used_at),
|
||
}
|
||
|
||
|
||
# ---- 用户管理 ----
|
||
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: str | None = None
|
||
role: str | None = None
|
||
is_active: bool | None = None
|
||
remark: str | None = None
|
||
custom_permissions: list[str] | None = None
|
||
|
||
|
||
class UserRename(BaseModel):
|
||
username: str = Field(..., min_length=2, max_length=64)
|
||
|
||
|
||
# ---- 账号 ----
|
||
class AccountImport(BaseModel):
|
||
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码|标签(可选)
|
||
|
||
tag 为本次导入的统一标签兜底:某行未单独写标签时使用该值,
|
||
行内第5列标签优先。
|
||
"""
|
||
|
||
text: str
|
||
tag: str = ""
|
||
|
||
|
||
class AccountAssign(BaseModel):
|
||
assigned_to: int | None = None
|
||
|
||
|
||
class AccountBulkSelection(BaseModel):
|
||
"""批量操作选择范围:显式 ID 或当前筛选条件下全部账号。"""
|
||
|
||
account_ids: list[int] = Field(default_factory=list)
|
||
all_matching: bool = False
|
||
search: str = ""
|
||
tag: str = ""
|
||
#: 多行斗鱼账号名(每行一个),与其它筛选条件叠加
|
||
account_names: str = ""
|
||
assigned_only: bool = False
|
||
#: 归属筛选:""=全部 / unassigned=未分配 / assigned=已分配
|
||
assign_status: str = ""
|
||
#: 只看某个客服名下的账号(与 assign_status 可叠加)
|
||
assigned_user_id: int | None = None
|
||
#: 分配池口径:持 CK 且 CK 未失效(排除停用/归档)
|
||
assignable_only: bool = False
|
||
has_cookie: bool = False
|
||
status: str = ""
|
||
ck_status: str = ""
|
||
failure_kind: str = ""
|
||
bind_status: str = ""
|
||
high_value_filter: str = ""
|
||
min_points: int | None = None
|
||
max_points: int | None = None
|
||
min_gold: float | None = None
|
||
max_gold: float | None = None
|
||
elite_opened: bool | None = None
|
||
esports_opened: bool | None = None
|
||
role_bind_status: str = ""
|
||
|
||
|
||
class AccountExportRequest(AccountBulkSelection):
|
||
"""账号导出请求:支持纯账号名单(txt)或结构化明细(csv)。"""
|
||
|
||
format: str = Field("txt", pattern="^(txt|csv)$")
|
||
|
||
|
||
class BatchAssign(AccountBulkSelection):
|
||
"""批量分配 / 取消分配。
|
||
|
||
⚠️ `assigned_to` 是**目标**客服(None=取消分配),与父类的
|
||
`assigned_user_id`(筛选「谁名下的账号」)是两件事,别混。
|
||
`all_matching=True` 时按父类的筛选条件解析目标账号。
|
||
"""
|
||
|
||
assigned_to: int | None = None
|
||
|
||
|
||
class AccountBulkTag(AccountBulkSelection):
|
||
tag_value: str = ""
|
||
|
||
|
||
class AccountTag(BaseModel):
|
||
tag: str | None = None
|
||
account_ids: list[int] | None = None
|
||
|
||
|
||
class AccountRemark(BaseModel):
|
||
remark: str | None = None
|
||
|
||
|
||
class AccountLifecycleUpdate(BaseModel):
|
||
"""人工设定生命周期状态。
|
||
|
||
`status` 取 `disabled`(停用)/ `archived`(归档)/ `auto`(恢复自动推导)。
|
||
"""
|
||
|
||
status: str = Field(..., pattern="^(disabled|archived|auto)$")
|
||
reason: str = ""
|
||
|
||
|
||
class AccountAutomationUpdate(BaseModel):
|
||
"""自动化开关:false = 账号可用,但自动化不碰它(人工维护的特殊账号)。"""
|
||
|
||
auto_managed: bool
|
||
|
||
|
||
class AccountOut(BaseModel):
|
||
id: int
|
||
username: str
|
||
# 敏感字段根据角色决定是否返回
|
||
password: str | None = None
|
||
email: str | None = None
|
||
email_password: str | None = None
|
||
tag: str = ""
|
||
assigned_to: int | None = None
|
||
assigned_username: str | None = None
|
||
remark: str = ""
|
||
created_at: datetime | None = None
|
||
# 生命周期状态(2026-10-04 S3):客服最关心的是「哪些要人工」
|
||
status: str = ""
|
||
status_label: str = ""
|
||
status_reason: str = ""
|
||
auto_managed: bool = True
|
||
# 登录失败分型(2026-10-07):客服排查重登/登录异常
|
||
relogin_failure_kind: str = ""
|
||
relogin_failure_label: str = ""
|
||
relogin_last_error: str = ""
|
||
# 列表页的「关键列」:全部直接来自 accounts 行,不需要任何 JOIN 或聚合
|
||
# (客服一眼要能看出「这账号能不能用、要不要我处理」)。
|
||
has_cookie: bool = False
|
||
ck_check_status: str = ""
|
||
ck_checked_at: datetime | None = None
|
||
points: int | None = None
|
||
gold_balance: int | None = None
|
||
role_bind_status: str = "unknown"
|
||
role_bind_label: str = ""
|
||
esports_bind_status: str = "unknown"
|
||
esports_bind_label: str = ""
|
||
xpd_bind_status: str = "unknown"
|
||
xpd_bind_label: str = ""
|
||
# 兑换记录(来自 account_events 的 exchange 事件,每页一条聚合查询)
|
||
exchange_count: int = 0
|
||
last_exchange_at: datetime | None = None
|
||
last_exchange_name: str = ""
|
||
# 一键查询得到的(账号表里本来没有的)信息,供列表展示。
|
||
# ⚠️ 必须在这里声明:Pydantic 默认 `extra='ignore'`,
|
||
# 没声明的字段会被**静默丢掉** —— 路由明明返回了,接口却看不到。
|
||
# 为 None 表示未查询;为 [] 表示已查询且两套均未兑换。
|
||
high_value_owned: list[str] | None = None
|
||
# 「有没有做过一键查询」靠它判断(列表不返回整份快照)
|
||
overview_queried_at: datetime | None = 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": _isoformat(self.created_at),
|
||
"status": self.status,
|
||
"status_label": self.status_label,
|
||
"status_reason": self.status_reason,
|
||
"auto_managed": self.auto_managed,
|
||
"relogin_failure_kind": self.relogin_failure_kind,
|
||
"relogin_failure_label": self.relogin_failure_label,
|
||
"relogin_last_error": self.relogin_last_error,
|
||
"has_cookie": self.has_cookie,
|
||
"ck_check_status": self.ck_check_status,
|
||
"ck_checked_at": _isoformat(self.ck_checked_at),
|
||
"points": self.points,
|
||
"gold_balance": self.gold_balance,
|
||
"role_bind_status": self.role_bind_status,
|
||
"role_bind_label": self.role_bind_label,
|
||
"esports_bind_status": self.esports_bind_status,
|
||
"esports_bind_label": self.esports_bind_label,
|
||
"xpd_bind_status": self.xpd_bind_status,
|
||
"xpd_bind_label": self.xpd_bind_label,
|
||
"exchange_count": self.exchange_count,
|
||
"last_exchange_at": _isoformat(self.last_exchange_at),
|
||
"last_exchange_name": self.last_exchange_name,
|
||
# ⚠️ 这个 `_serialize` 是**手写**的:字段只声明在模型上**不够**,
|
||
# 不在这里列出来就会被丢掉。踩过 —— 路由返回的对象属性明明有值,
|
||
# 前端拿到的 JSON 里却没有(属性访问能过、序列化后没有,
|
||
# 只断言属性的测试因此假绿)。
|
||
"high_value_owned": self.high_value_owned,
|
||
"overview_queried_at": _isoformat(self.overview_queried_at),
|
||
}
|
||
|
||
|
||
# ---- 登录任务 ----
|
||
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 CookieReloginRequest(BaseModel):
|
||
ids: list[int] = [] # 需要重新登录的**账号** ID(2026-10-04 CK 解耦前是记录 ID)
|
||
|
||
|
||
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: datetime | None = None
|
||
finished_at: datetime | None = 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": _isoformat(self.created_at),
|
||
"finished_at": _isoformat(self.finished_at),
|
||
}
|
||
|
||
|
||
# ---- 账号检测 ----
|
||
class AccountCheckBatchRequest(BaseModel):
|
||
"""两种来源二选一(**旧的粘贴输入完整保留**)。
|
||
|
||
- `text`:手动粘贴(旧模式)—— 条目没有 account_id,结果只在批次里与 zip 里,
|
||
不写账号时间线(保持旧行为);
|
||
- `account_ids`:按账号选择(新模式)—— 凭据从库里取,结果写回账号时间线。
|
||
"""
|
||
|
||
text: str = ""
|
||
# 「选择账号」的两种选法(与账号列表的批量操作保持同一套字段):
|
||
# ① 手动挑几个 → account_ids;② 按筛选全选 → select_all_matching + 各筛选条件
|
||
account_ids: list[int] = Field(default_factory=list)
|
||
select_all_matching: bool = False
|
||
search: str = ""
|
||
tag: str = ""
|
||
status: str = ""
|
||
ck_status: str = ""
|
||
bind_status: str = ""
|
||
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
|
||
# 登录接口策略,与登录任务同一套取值:wgapi(默认,保持原行为)或 iframe。
|
||
# 有些人只有其中一套能收到邮箱验证码,所以要能选。
|
||
api_strategy: str = "wgapi"
|
||
|
||
|
||
class AccountCheckItemOut(BaseModel):
|
||
line: int
|
||
username: str
|
||
email: str
|
||
account_id: int | None = None
|
||
status: str
|
||
message: str
|
||
started_at: datetime | None = None
|
||
finished_at: datetime | None = 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
|
||
api_strategy: str = "wgapi"
|
||
total: int
|
||
finished_count: int
|
||
running_count: int
|
||
status_counts: dict[str, int]
|
||
created_at: datetime | None = None
|
||
started_at: datetime | None = None
|
||
finished_at: datetime | None = None
|
||
items: list[AccountCheckItemOut]
|
||
|
||
|
||
# ---- 虎牙 ----
|
||
class HuyaCookieImport(BaseModel):
|
||
"""批量导入虎牙 Cookie。支持纯 CK 或 账号----密码----CK。"""
|
||
|
||
text: str
|
||
tag: str = ""
|
||
|
||
|
||
class HuyaPasswordLoginRequest(BaseModel):
|
||
"""虎牙账号 Web 密码登录并保存 Cookie(旧版)。"""
|
||
|
||
username: str = Field(..., min_length=1, max_length=128)
|
||
password: str = Field(..., min_length=1, max_length=128)
|
||
tag: str = ""
|
||
cookie: str = ""
|
||
|
||
|
||
class HuyaAppPasswordLoginRequest(BaseModel):
|
||
"""虎牙账号 App 协议密码登录并保存 Cookie(推荐)。"""
|
||
|
||
username: str = Field(..., min_length=1, max_length=128)
|
||
password: str = Field(..., min_length=1, max_length=128)
|
||
tag: str = ""
|
||
force_new_device: bool = False
|
||
|
||
|
||
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 HuyaTestSmsUrlRequest(BaseModel):
|
||
"""在线测试从接码链接提取验证码。"""
|
||
|
||
url: str = Field(..., min_length=1)
|
||
phone: 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 HuyaAutoRegisterRetryRequest(BaseModel):
|
||
"""继续批次。默认 continue=从停止处往下跑(不重试已失败)。
|
||
|
||
mode:
|
||
- continue: 只跑 pending/stopped/中断中(默认)
|
||
- retry_failed: 只重试 error
|
||
- all_unfinished: 失败+未完成都跑
|
||
"""
|
||
|
||
mode: str = Field("continue", max_length=32)
|
||
concurrency: int | None = Field(None, ge=1, le=5)
|
||
wait_seconds: float | None = Field(None, ge=15, le=600)
|
||
poll_interval: float | None = Field(None, ge=1, le=30)
|
||
password_prefix: str | None = Field(None, max_length=8)
|
||
fixed_password: str | None = Field(None, max_length=64)
|
||
use_proxy: bool | None = None
|
||
|
||
|
||
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: int | None = None
|
||
username: str = ""
|
||
uid: str = ""
|
||
password: str = ""
|
||
password_changed: bool = False
|
||
cookie: str = ""
|
||
cookie_preview: str = ""
|
||
started_at: datetime | None = None
|
||
finished_at: datetime | None = 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: datetime | None = None
|
||
started_at: datetime | None = None
|
||
finished_at: datetime | None = None
|
||
items: list[HuyaAutoRegisterItemOut] = []
|
||
|
||
|
||
class HuyaRegisterSuccessLogOut(BaseModel):
|
||
id: int
|
||
batch_id: str = ""
|
||
item_id: int | None = None
|
||
account_id: int | None = None
|
||
phone: str = ""
|
||
username: str = ""
|
||
uid: str = ""
|
||
password: str = ""
|
||
sms_url: str = ""
|
||
tag: str = ""
|
||
provider: str = ""
|
||
created_by: int | None = None
|
||
created_at: datetime | None = None
|
||
export_line: str = ""
|
||
|
||
|
||
class HuyaPasswordAccountImport(BaseModel):
|
||
"""导入虎牙账号密码,稍后再选择登录。"""
|
||
|
||
text: str = Field(..., min_length=1)
|
||
tag: str = ""
|
||
|
||
|
||
class HuyaPasswordLoginSelectedRequest(BaseModel):
|
||
"""选择已导入的虎牙账号执行密码登录。"""
|
||
|
||
account_ids: list[int] = Field(default_factory=list)
|
||
all_matching: bool = False
|
||
search: str = ""
|
||
tag: str = ""
|
||
assigned_only: bool = False
|
||
has_cookie: bool = False
|
||
force_new_device: bool = False
|
||
max_attempts: int = Field(2, ge=1, le=3)
|
||
interval_seconds: float = Field(8, ge=0, le=60)
|
||
retry_delay_seconds: float = Field(3, ge=0, le=30)
|
||
|
||
|
||
class HuyaDeviceBindingsBatchDeleteRequest(BaseModel):
|
||
"""批量解绑设备:删除账号画像与 hydevice 指纹状态。"""
|
||
|
||
accounts: list[str] = Field(..., min_length=1)
|
||
|
||
|
||
class HuyaAccountOut(BaseModel):
|
||
id: int
|
||
uid: str = ""
|
||
yyuid: str = ""
|
||
username: str = ""
|
||
has_password: bool = False
|
||
has_cookie: bool = False
|
||
nickname: str = ""
|
||
cookie: str = ""
|
||
cookie_preview: str = ""
|
||
tag: str = ""
|
||
remark: str = ""
|
||
status: str = ""
|
||
# 上次成功登录渠道: web / app / sms (空 = 仅导入 Cookie, 从未协议登录)
|
||
login_channel: str = ""
|
||
points: int | None = None
|
||
game_name: str = ""
|
||
game_channel: str = ""
|
||
game_phone: str = ""
|
||
# 精英宝典开通状态(NULL = 未查询);有效期接口没给时为空
|
||
elite_opened: bool | None = None
|
||
elite_open_time: int | None = None
|
||
elite_expire_time: int | None = None
|
||
sms_url: str = ""
|
||
assigned_to: int | None = None
|
||
assigned_username: str | None = None
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = 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,
|
||
"has_cookie": self.has_cookie,
|
||
"nickname": self.nickname,
|
||
"cookie": self.cookie,
|
||
"cookie_preview": self.cookie_preview,
|
||
"tag": self.tag,
|
||
"remark": self.remark,
|
||
"status": self.status,
|
||
"login_channel": self.login_channel,
|
||
"points": self.points,
|
||
"game_name": self.game_name,
|
||
"game_channel": self.game_channel,
|
||
"game_phone": self.game_phone,
|
||
"elite_opened": self.elite_opened,
|
||
"elite_open_time": self.elite_open_time,
|
||
"elite_expire_time": self.elite_expire_time,
|
||
"sms_url": self.sms_url,
|
||
"assigned_to": self.assigned_to,
|
||
"assigned_username": self.assigned_username,
|
||
"created_at": _isoformat(self.created_at),
|
||
"updated_at": _isoformat(self.updated_at),
|
||
}
|
||
|
||
|
||
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: datetime | None = 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": _isoformat(self.updated_at),
|
||
}
|
||
|
||
|
||
class HuyaConfigUpdate(BaseModel):
|
||
room_pid: str | None = None
|
||
sid: str | None = None
|
||
outer_act_id: str | None = None
|
||
bind_act_id: str | None = None
|
||
pay_channel: str | None = None
|
||
|
||
|
||
class HuyaTaskBatchRequest(BaseModel):
|
||
account_ids: list[int]
|
||
task_type: str
|
||
handbook_scope: str = Field("legacy", pattern="^(legacy|elite)$")
|
||
concurrency: int = 3
|
||
payload: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
class HuyaWorkbenchAccountsUpdate(BaseModel):
|
||
handbook_scope: str = Field(..., pattern="^elite$|^browser_exchange$")
|
||
account_ids: list[int] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaWorkbenchCookieImport(BaseModel):
|
||
"""工作台「导入外部 CK」:把外部 Cookie 入库并直接加入自己的工作台。"""
|
||
|
||
text: str = Field(..., min_length=1)
|
||
tag: str = ""
|
||
handbook_scope: str = Field("elite", pattern="^elite$|^browser_exchange$")
|
||
|
||
|
||
class HuyaBrowserExchangeRequest(BaseModel):
|
||
"""浏览器兑换的目标页选择(常量见 services/huya_exchange_targets.py)。"""
|
||
|
||
target: str | None = Field(None, pattern="^(live_room|elite|esports)$")
|
||
|
||
|
||
class HuyaTaskOut(BaseModel):
|
||
id: int
|
||
batch_id: str
|
||
account_id: int
|
||
account_uid: str = ""
|
||
account_nickname: str = ""
|
||
task_type: str
|
||
handbook_scope: str = "legacy"
|
||
status: str
|
||
message: str = ""
|
||
result: dict[str, Any] | None = None
|
||
created_by: int
|
||
created_at: datetime | None = None
|
||
finished_at: datetime | None = 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,
|
||
"handbook_scope": self.handbook_scope,
|
||
"status": self.status,
|
||
"message": self.message,
|
||
"result": self.result,
|
||
"created_by": self.created_by,
|
||
"created_at": _isoformat(self.created_at),
|
||
"finished_at": _isoformat(self.finished_at),
|
||
}
|
||
|
||
|
||
class HuyaGoodsOut(BaseModel):
|
||
id: int
|
||
account_id: int | None = None
|
||
sid: int = 0
|
||
product_id: str
|
||
name: str = ""
|
||
price: int | None = None
|
||
remain_text: str = ""
|
||
raw: dict[str, Any] | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
@model_serializer
|
||
def _serialize(self) -> dict[str, Any]:
|
||
return {
|
||
"id": self.id,
|
||
"account_id": self.account_id,
|
||
"sid": self.sid,
|
||
"product_id": self.product_id,
|
||
"name": self.name,
|
||
"price": self.price,
|
||
"remain_text": self.remain_text,
|
||
"raw": self.raw,
|
||
"updated_at": _isoformat(self.updated_at),
|
||
}
|
||
|
||
|
||
class HuyaRechargeGoodsOut(BaseModel):
|
||
id: int
|
||
account_id: int | None = None
|
||
sid: int = 0
|
||
spu_id: str
|
||
sku_id: str = ""
|
||
name: str = ""
|
||
price: int | None = None
|
||
stock: int | None = None
|
||
buy_limit: int | None = None
|
||
icon: str = ""
|
||
description: str = ""
|
||
task_id: str = ""
|
||
task_name: str = ""
|
||
raw: dict[str, Any] | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
@model_serializer
|
||
def _serialize(self) -> dict[str, Any]:
|
||
return {
|
||
"id": self.id,
|
||
"account_id": self.account_id,
|
||
"sid": self.sid,
|
||
"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": _isoformat(self.updated_at),
|
||
}
|
||
|
||
|
||
# ---- 斗鱼活动任务 ----
|
||
class DouyuConfigOut(BaseModel):
|
||
manual_id: str = "G4KA4Qnz4LDp7"
|
||
rid: str = "9263298"
|
||
bind_act_alias: str = "20260120QYOOB"
|
||
confirm_act_alias: str = "20260120QYOOB"
|
||
legacy_act_alias: str = "cjm"
|
||
room_id: str = "9263298"
|
||
elite_amount: int = 3000
|
||
esports_manual_id: str = "1W0yGkOdDbAXb"
|
||
esports_act_alias: str = "20260226ZMGDG"
|
||
esports_amount: int = 3000
|
||
esports_chicken_gift_id: str = "24766"
|
||
esports_chicken_skin_id: str = "0"
|
||
esports_firework_gift_id: str = "24767"
|
||
esports_firework_skin_id: str = "3850"
|
||
peace_enabled: bool = False
|
||
xpd_act_alias: str = "20260623KDQFH"
|
||
xpd_act_id: str = "46195"
|
||
xpd_rid: str = "9263298"
|
||
gold_pay_type: int = 1
|
||
gold_recharge_channel: str = "wechat_qr"
|
||
gold_api_product_id: str = "111570"
|
||
gold_api_account_template_name: str = "斗鱼昵称"
|
||
gift_id: str = "23643"
|
||
skin_id: str = "2942"
|
||
updated_at: datetime | None = None
|
||
|
||
@model_serializer
|
||
def _serialize(self) -> dict[str, Any]:
|
||
return {
|
||
"manual_id": self.manual_id,
|
||
"rid": self.rid,
|
||
"bind_act_alias": self.bind_act_alias,
|
||
"confirm_act_alias": self.confirm_act_alias,
|
||
"legacy_act_alias": self.legacy_act_alias,
|
||
"room_id": self.room_id,
|
||
"elite_amount": self.elite_amount,
|
||
"esports_manual_id": self.esports_manual_id,
|
||
"esports_act_alias": self.esports_act_alias,
|
||
"esports_amount": self.esports_amount,
|
||
"esports_chicken_gift_id": self.esports_chicken_gift_id,
|
||
"esports_chicken_skin_id": self.esports_chicken_skin_id,
|
||
"esports_firework_gift_id": self.esports_firework_gift_id,
|
||
"esports_firework_skin_id": self.esports_firework_skin_id,
|
||
"peace_enabled": self.peace_enabled,
|
||
"xpd_act_alias": self.xpd_act_alias,
|
||
"xpd_act_id": self.xpd_act_id,
|
||
"xpd_rid": self.xpd_rid,
|
||
"gold_pay_type": self.gold_pay_type,
|
||
"gold_recharge_channel": self.gold_recharge_channel,
|
||
"gold_api_product_id": self.gold_api_product_id,
|
||
"gold_api_account_template_name": self.gold_api_account_template_name,
|
||
"gift_id": self.gift_id,
|
||
"skin_id": self.skin_id,
|
||
"updated_at": _isoformat(self.updated_at),
|
||
}
|
||
|
||
|
||
class DouyuConfigUpdate(BaseModel):
|
||
manual_id: str | None = None
|
||
rid: str | None = None
|
||
bind_act_alias: str | None = None
|
||
confirm_act_alias: str | None = None
|
||
legacy_act_alias: str | None = None
|
||
room_id: str | None = None
|
||
elite_amount: int | None = Field(None, ge=1)
|
||
esports_manual_id: str | None = None
|
||
esports_act_alias: str | None = None
|
||
esports_amount: int | None = Field(None, ge=1)
|
||
esports_chicken_gift_id: str | None = None
|
||
esports_chicken_skin_id: str | None = None
|
||
esports_firework_gift_id: str | None = None
|
||
esports_firework_skin_id: str | None = None
|
||
peace_enabled: bool | None = None
|
||
xpd_act_alias: str | None = None
|
||
xpd_act_id: str | None = None
|
||
xpd_rid: str | None = None
|
||
gold_pay_type: int | None = Field(None, ge=1, le=9)
|
||
gold_recharge_channel: str | None = Field(
|
||
None, pattern="^(wechat_qr|supplier_api)$"
|
||
)
|
||
gold_api_product_id: str | None = Field(None, max_length=128)
|
||
gold_api_account_template_name: str | None = Field(None, max_length=64)
|
||
gift_id: str | None = None
|
||
skin_id: str | None = None
|
||
|
||
|
||
class DouyuTaskBatchRequest(BaseModel):
|
||
account_ids: list[int]
|
||
task_type: str
|
||
handbook_scope: str = Field(..., pattern="^(elite|esports|peace)$")
|
||
concurrency: int = Field(3, ge=1, le=10)
|
||
payload: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
class DouyuWorkbenchAccountsUpdate(BaseModel):
|
||
handbook_scope: str = Field(..., pattern="^(elite|esports|peace)$")
|
||
account_ids: list[int] = Field(default_factory=list)
|
||
|
||
|
||
class DouyuTaskOut(BaseModel):
|
||
id: int
|
||
batch_id: str
|
||
account_id: int
|
||
account_username: str = ""
|
||
account_uid: str = ""
|
||
account_nickname: str = ""
|
||
task_type: str
|
||
handbook_scope: str = "legacy"
|
||
status: str
|
||
message: str = ""
|
||
result: dict[str, Any] | None = None
|
||
created_by: int
|
||
created_at: datetime | None = None
|
||
finished_at: datetime | None = 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,
|
||
"account_uid": self.account_uid,
|
||
"account_nickname": self.account_nickname,
|
||
"task_type": self.task_type,
|
||
"handbook_scope": self.handbook_scope,
|
||
"status": self.status,
|
||
"message": self.message,
|
||
"result": self.result,
|
||
"created_by": self.created_by,
|
||
"created_at": _isoformat(self.created_at),
|
||
"finished_at": _isoformat(self.finished_at),
|
||
}
|
||
|
||
|
||
class DouyuGoodsOut(BaseModel):
|
||
id: int
|
||
commodity_id: str
|
||
name: str = ""
|
||
score: int | None = None
|
||
status: str = ""
|
||
raw: dict[str, Any] | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
@model_serializer
|
||
def _serialize(self) -> dict[str, Any]:
|
||
return {
|
||
"id": self.id,
|
||
"commodity_id": self.commodity_id,
|
||
"name": self.name,
|
||
"score": self.score,
|
||
"status": self.status,
|
||
"raw": self.raw,
|
||
"updated_at": _isoformat(self.updated_at),
|
||
}
|
||
|
||
|
||
class DouyuTaskAccountOut(BaseModel):
|
||
id: int
|
||
username: str
|
||
uid: str = ""
|
||
nickname: str = ""
|
||
tag: str = ""
|
||
points: int | None = None
|
||
game_name: str = ""
|
||
game_channel: str = ""
|
||
gold_balance: int | None = None
|
||
exchange_balance: int | None = None
|
||
# 绑定状态:三列各管一个作用域,取值 unknown/unbound/pending/bound
|
||
# (见 services/account_binding)。**`bind_status` 已废弃**,不再返回 ——
|
||
# 它是「最后发生的动作」式复用列,前端拿它判断绑定必然出错。
|
||
role_bind_status: str = "unknown"
|
||
role_bind_label: str = ""
|
||
change_role_wait_time: int | None = None
|
||
elite_opened: bool | None = None
|
||
elite_expire_time: int | None = None
|
||
esports_points: int | None = None
|
||
esports_game_name: str = ""
|
||
esports_game_channel: str = ""
|
||
esports_bind_status: str = "unknown"
|
||
esports_bind_label: str = ""
|
||
esports_change_role_wait_time: int | None = None
|
||
esports_can_change_time: int | None = None
|
||
xpd_game_name: str = ""
|
||
xpd_openid: str = ""
|
||
xpd_role_id: str = ""
|
||
xpd_plat_id: int | None = None
|
||
xpd_area_id: int | None = None
|
||
xpd_balance: int | None = None
|
||
xpd_fragments: int | None = None
|
||
xpd_bind_status: str = "unknown"
|
||
xpd_bind_label: str = ""
|
||
assigned_to: int | None = None
|
||
assigned_username: str | None = None
|
||
|
||
|
||
class DouyuXpdGoodsOut(BaseModel):
|
||
id: int
|
||
commodity_id: str
|
||
name: str = ""
|
||
price: int | None = None
|
||
org_price: int | None = None
|
||
category: str = ""
|
||
goods_left: int | None = None
|
||
raw: dict[str, Any] | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
@model_serializer
|
||
def _serialize(self) -> dict[str, Any]:
|
||
return {
|
||
"id": self.id,
|
||
"commodity_id": self.commodity_id,
|
||
"name": self.name,
|
||
"price": self.price,
|
||
"org_price": self.org_price,
|
||
"category": self.category,
|
||
"goods_left": self.goods_left,
|
||
"raw": self.raw,
|
||
"updated_at": _isoformat(self.updated_at),
|
||
}
|
||
|
||
|
||
# ---- 代理配置 ----
|
||
class ProxyConfigOut(BaseModel):
|
||
enabled: bool = False
|
||
api_url: str = ""
|
||
http: str = ""
|
||
https: str = ""
|
||
whitelist_enabled: bool = False
|
||
whitelist_platform: str = "xiequ"
|
||
whitelist_credentials: dict | None = None
|
||
# 旧字段保留(向后兼容)
|
||
whitelist_uid: str = ""
|
||
whitelist_ukey: str = ""
|
||
|
||
|
||
class ProxyConfigUpdate(BaseModel):
|
||
enabled: bool | None = None
|
||
api_url: str | None = None
|
||
http: str | None = None
|
||
https: str | None = None
|
||
whitelist_enabled: bool | None = None
|
||
whitelist_platform: str | None = "xiequ"
|
||
whitelist_credentials: dict | None = None
|
||
# 旧字段保留(向后兼容)
|
||
whitelist_uid: str | None = None
|
||
whitelist_ukey: str | None = 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
|
||
|
||
|
||
# ---- 文件上传(在线协作图片) ----
|
||
class FileAssetOut(BaseModel):
|
||
"""上传结果。url 一律是本服务的代理地址,不下发对象存储直链。"""
|
||
|
||
object_key: str
|
||
url: str
|
||
thumbnail_url: str
|
||
medium_url: str
|
||
filename: str
|
||
content_type: str
|
||
size: int
|
||
|
||
|
||
class HuyaCollabShopCreate(BaseModel):
|
||
"""新增店铺(字典),新建协作单时就能点选。"""
|
||
|
||
name: str = Field(..., min_length=1, max_length=64)
|
||
sort_order: int = 0
|
||
remark: str = Field("", max_length=255)
|
||
|
||
|
||
class HuyaCollabShopUpdate(BaseModel):
|
||
"""改店铺:名字 / 排序 / 停用启用 / 备注(只提交要改的字段)。"""
|
||
|
||
name: str | None = Field(None, min_length=1, max_length=64)
|
||
sort_order: int | None = None
|
||
is_active: bool | None = None
|
||
remark: str | None = Field(None, max_length=255)
|
||
# 改名时是否把已有订单里的旧名字一起改掉(默认改,避免筛选里堆一堆改名前的老值)
|
||
rename_orders: bool = True
|
||
|
||
|
||
class HuyaCollabShopOut(BaseModel):
|
||
"""店铺字典项。"""
|
||
|
||
id: int
|
||
name: str
|
||
sort_order: int = 0
|
||
is_active: bool = True
|
||
remark: str = ""
|
||
created_by_name: str = ""
|
||
updated_by_name: str = ""
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
|
||
# ---- 虎牙外部 CK 协作台账(见 docs/设计-虎牙外部CK协作台账.md) ----
|
||
class HuyaCollabGiftOut(BaseModel):
|
||
"""活动礼包(用一份有效 CK 现拉,不落库)。"""
|
||
|
||
product_id: str = ""
|
||
name: str = ""
|
||
price: int | None = None
|
||
remain_text: str = ""
|
||
stock: int | None = None
|
||
score: int | None = None
|
||
category_name: str = ""
|
||
icon: str = ""
|
||
|
||
|
||
class HuyaCollabCkPreviewRequest(BaseModel):
|
||
"""粘贴 CK 后的即时预览(只读:探针 + 顺带拉礼包)。"""
|
||
|
||
ck_text: str = Field(..., min_length=1)
|
||
with_gifts: bool = True
|
||
|
||
|
||
class HuyaCollabBuyerOrderOut(BaseModel):
|
||
"""「同一买家的其它单」里的一条(够列表/详情里一眼看清是哪单)。"""
|
||
|
||
id: int
|
||
order_no: str
|
||
shop: str = ""
|
||
gift_name: str = ""
|
||
uid: str = ""
|
||
status: str = "incomplete"
|
||
status_label: str = ""
|
||
created_at: datetime | None = None
|
||
|
||
|
||
class HuyaCollabCkPreviewOut(BaseModel):
|
||
"""预览结论:``ok`` 且 ``has_role`` 才允许提交新建。"""
|
||
|
||
ok: bool = False
|
||
message: str = ""
|
||
uid: str = ""
|
||
yyuid: str = ""
|
||
hy_username: str = ""
|
||
ck_status: str = "unverified"
|
||
bind_game_account: str = ""
|
||
bind_game_role: str = ""
|
||
bind_game_channel: str = ""
|
||
has_role: bool = False
|
||
gifts: list[HuyaCollabGiftOut] = Field(default_factory=list)
|
||
gifts_message: str = ""
|
||
# 这个虎牙号已经挂在哪些单上(放开 uid 唯一性后只是提示,不拦提交)
|
||
uid_used_by: list[HuyaCollabBuyerOrderOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderCreate(BaseModel):
|
||
"""新建一行(可同时贴一份外部 CK)。
|
||
|
||
``require_verified``(默认开):人工新建表单的严格模式 —— 订单编号 / 店铺 / CK /
|
||
礼包名 / **主页截图**必填,且 CK 必须探针通并读到绑定角色;粘贴导入不走这条路。
|
||
|
||
``draft``(默认关):先建单后补资料 —— 只登记订单号 + 店铺,状态进「待补资料」,
|
||
CK / 礼包名 / 主页截图都可以稍后在详情里补(2026-10-17 用户需求)。
|
||
"""
|
||
|
||
order_no: str = Field(..., min_length=1, max_length=64)
|
||
shop: str = Field("", max_length=64)
|
||
require_verified: bool = True
|
||
draft: bool = False
|
||
owner_id: int | None = None
|
||
ck_text: str = ""
|
||
bind_douyu_account: str = Field("", max_length=128)
|
||
gift_name: str = Field("", max_length=128)
|
||
required_points: int | None = Field(None, ge=0)
|
||
# 主页截图(object_key 数组,第一张作封面)。绑定图不再收:绑定关系由探针自动读,
|
||
# 人工截图没有意义(2026-10-16 用户拍板)。
|
||
home_images: list[str] = Field(default_factory=list)
|
||
remark: str = Field("", max_length=512)
|
||
|
||
|
||
class HuyaExternalOrderUpdate(BaseModel):
|
||
"""部分更新:只提交要改的字段(服务端用 ``exclude_unset`` 识别)。"""
|
||
|
||
order_no: str | None = Field(None, max_length=64)
|
||
shop: str | None = Field(None, max_length=64)
|
||
owner_id: int | None = None
|
||
bind_douyu_account: str | None = Field(None, max_length=128)
|
||
bind_douyu_uid: str | None = Field(None, max_length=32)
|
||
gift_name: str | None = Field(None, max_length=128)
|
||
required_points: int | None = Field(None, ge=0)
|
||
remark: str | None = Field(None, max_length=512)
|
||
# 乐观锁:客户端拿到的版本号。不一致说明别人先改了 → 409。
|
||
expected_version: int | None = None
|
||
|
||
|
||
class HuyaExternalOrderStatusRequest(BaseModel):
|
||
"""推进/回退业务状态。"""
|
||
|
||
status: str = Field(..., min_length=1, max_length=16)
|
||
reason: str = Field("", max_length=256)
|
||
# 目标不是「向前一步」时必须显式置 True,避免误点把状态改回去。
|
||
rollback: bool = False
|
||
expected_version: int | None = None
|
||
|
||
|
||
class HuyaExternalOrderArchiveRequest(BaseModel):
|
||
"""归档抢购成功的订单(单条/勾选/按当前筛选)。"""
|
||
|
||
ids: list[int] = Field(default_factory=list)
|
||
reason: str = Field("", max_length=256)
|
||
# true 时忽略 ids,按下面的筛选条件归档所有匹配的「抢购成功」活跃行。
|
||
archive_filtered: bool = False
|
||
expected_count: int | None = Field(None, ge=0)
|
||
shop: str = Field("", max_length=64)
|
||
owner_id: int | None = None
|
||
status: str = Field("", max_length=16)
|
||
ck_status: str = Field("", max_length=16)
|
||
gift_name: str = Field("", max_length=128)
|
||
keyword: str = Field("", max_length=128)
|
||
include_void: bool = False
|
||
|
||
|
||
class HuyaExternalOrderUnarchiveRequest(BaseModel):
|
||
"""取消归档(恢复回主台账)。"""
|
||
|
||
ids: list[int] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderCkRequest(BaseModel):
|
||
"""贴入/换 CK,或从 raw_cookie 重建。"""
|
||
|
||
ck_text: str = ""
|
||
action: str = Field("paste", pattern="^(paste|rebuild)$")
|
||
expected_version: int | None = None
|
||
|
||
|
||
class HuyaExternalOrderCkStatusRequest(BaseModel):
|
||
"""人工改 CK 状态:**只能**改成「已停用」或「未验证」。
|
||
|
||
``valid`` / ``invalid`` 由只读探针给(``POST .../verify``)——人工标「有效」等于绕过验证。
|
||
"""
|
||
|
||
status: str = Field(..., pattern="^(unverified|disabled)$")
|
||
message: str = Field("", max_length=256)
|
||
expected_version: int | None = None
|
||
|
||
|
||
class HuyaExternalOrderImagesRequest(BaseModel):
|
||
"""整槽替换某个图片槽位(提交 /api/files/upload 返回的 object_key)。"""
|
||
|
||
slot: str = Field(..., pattern="^(bind_images|home_images)$")
|
||
images: list[str] = Field(default_factory=list)
|
||
expected_version: int | None = None
|
||
|
||
|
||
class HuyaExternalOrderImportRequest(BaseModel):
|
||
"""Excel 粘贴批量导入(TSV,首行可为表头)。"""
|
||
|
||
text: str
|
||
|
||
|
||
class HuyaExternalOrderVerifyRequest(BaseModel):
|
||
"""批量验证 CK(探针):人工勾选的行 id(一次最多 50 条)。"""
|
||
|
||
ids: list[int] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderVerifyItemOut(BaseModel):
|
||
"""批量验证里的一行结论(``ok=False`` 时看 ``message``)。"""
|
||
|
||
id: int
|
||
order_no: str = ""
|
||
ok: bool = False
|
||
ck_status: str = ""
|
||
message: str = ""
|
||
binding: dict[str, str] = Field(default_factory=dict)
|
||
|
||
|
||
class HuyaExternalOrderVerifyOut(BaseModel):
|
||
"""批量验证汇总:``valid + failed = total``。"""
|
||
|
||
total: int
|
||
valid: int
|
||
failed: int
|
||
results: list[HuyaExternalOrderVerifyItemOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderStatusBatchRequest(BaseModel):
|
||
"""勾选多行,一次切到同一个业务状态(单个也走这里,ids 只放一个)。"""
|
||
|
||
ids: list[int] = Field(default_factory=list)
|
||
status: str = Field(..., min_length=1, max_length=16)
|
||
reason: str = Field("", max_length=256)
|
||
# 目标不是「向前」时必须显式置 True(和单行接口一致,避免误点)
|
||
rollback: bool = False
|
||
|
||
|
||
class HuyaExternalOrderStatusBatchItemOut(BaseModel):
|
||
"""批量改状态里的一行结论(``ok=False`` 时看 ``message``,不影响其它行)。"""
|
||
|
||
id: int
|
||
order_no: str = ""
|
||
ok: bool = False
|
||
message: str = ""
|
||
|
||
|
||
class HuyaExternalOrderStatusBatchOut(BaseModel):
|
||
"""批量改状态汇总:``updated + failed = total``。"""
|
||
|
||
total: int
|
||
updated: int
|
||
failed: int
|
||
target: str = ""
|
||
target_label: str = ""
|
||
results: list[HuyaExternalOrderStatusBatchItemOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderRechargeRequest(BaseModel):
|
||
"""充值核账请求:按绑定账号整组核账,阈值默认 100。"""
|
||
|
||
ids: list[int] = Field(default_factory=list)
|
||
threshold: int = Field(100, ge=0, le=1_000_000)
|
||
|
||
|
||
class HuyaExternalOrderRechargeOrderOut(BaseModel):
|
||
id: int
|
||
order_no: str = ""
|
||
status: str = ""
|
||
status_label: str = ""
|
||
|
||
|
||
class HuyaExternalOrderRechargeGroupOut(BaseModel):
|
||
group_key: str = ""
|
||
status: str = ""
|
||
status_label: str = ""
|
||
balance: int | None = None
|
||
required_total: int | None = None
|
||
surplus: int | None = None
|
||
threshold: int = 100
|
||
reason: str = ""
|
||
message: str = ""
|
||
order_ids: list[int] = Field(default_factory=list)
|
||
orders: list[HuyaExternalOrderRechargeOrderOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderRechargeFailureOut(BaseModel):
|
||
id: int
|
||
order_no: str = ""
|
||
message: str = ""
|
||
|
||
|
||
class HuyaExternalOrderRechargeOut(BaseModel):
|
||
total: int
|
||
updated: int
|
||
failed: int
|
||
threshold: int = 100
|
||
groups: list[HuyaExternalOrderRechargeGroupOut] = Field(default_factory=list)
|
||
failures: list[HuyaExternalOrderRechargeFailureOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderArchiveItemOut(BaseModel):
|
||
"""归档/取消归档里的一行结论。"""
|
||
|
||
id: int
|
||
order_no: str = ""
|
||
ok: bool = False
|
||
message: str = ""
|
||
|
||
|
||
class HuyaExternalOrderArchiveOut(BaseModel):
|
||
"""归档/取消归档汇总:``archived`` 或 ``unarchived`` + skipped = total。"""
|
||
|
||
total: int
|
||
archived: int = 0
|
||
unarchived: int = 0
|
||
skipped: int = 0
|
||
results: list[HuyaExternalOrderArchiveItemOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderActivityQueryRequest(BaseModel):
|
||
"""勾选多行查积分 / 兑换记录(一次最多 50 条,串行带间隔)。
|
||
|
||
``scope``(2026-10-17 用户要求把批量按钮拆成两个):
|
||
|
||
- ``both``(默认,保持原行为):两个接口都查;
|
||
- ``score``:只查积分 —— 没有兑换记录就没有判定,**不会**自动改业务状态;
|
||
- ``records``:只查兑换记录 —— 积分不更新,判定与自动改状态照旧。
|
||
"""
|
||
|
||
ids: list[int] = Field(default_factory=list)
|
||
scope: str = "both"
|
||
|
||
|
||
class HuyaExternalOrderCkExportRequest(BaseModel):
|
||
"""勾选多行,导出这些行的 CK 明文(前端写剪贴板;一次最多 50 条)。"""
|
||
|
||
ids: list[int] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderCkExportItemOut(BaseModel):
|
||
"""导出结果里的一行:``ck_text`` 是**明文凭据**,只走这个接口、不留日志。"""
|
||
|
||
id: int
|
||
order_no: str = ""
|
||
uid: str = ""
|
||
nickname: str = ""
|
||
status: str = ""
|
||
status_label: str = ""
|
||
ck_status: str = ""
|
||
ck_status_label: str = ""
|
||
ck_text: str = ""
|
||
|
||
|
||
class HuyaExternalOrderCkExportSkippedOut(BaseModel):
|
||
"""没导出的行(例如这行还没贴 CK)——把原因显式回给前端,别静默丢。"""
|
||
|
||
id: int
|
||
order_no: str = ""
|
||
reason: str = ""
|
||
|
||
|
||
class HuyaExternalOrderCkExportOut(BaseModel):
|
||
"""导出 CK 的结果:``exported + len(skipped) = total``。"""
|
||
|
||
total: int
|
||
exported: int
|
||
skipped: list[HuyaExternalOrderCkExportSkippedOut] = Field(default_factory=list)
|
||
items: list[HuyaExternalOrderCkExportItemOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaCollabMetricMatchedOut(BaseModel):
|
||
"""匹配到的那条兑换记录(只留界面要用的几个字段)。"""
|
||
|
||
prize_name: str = ""
|
||
score: int | None = None
|
||
status_label: str = ""
|
||
order_id: str = ""
|
||
exchange_time: int | None = None
|
||
|
||
|
||
class HuyaCollabMetricOut(BaseModel):
|
||
"""一条「账号全生命周期」快照(积分 / 兑换记录 + 判定)。"""
|
||
|
||
id: int
|
||
order_id: int
|
||
order_no: str = ""
|
||
uid: str = ""
|
||
sid: int | None = None
|
||
# 这条快照查了哪一半(both / score / records):前端据此决定显示哪几段
|
||
scope: str = "both"
|
||
score: int | None = None
|
||
gained_score: int | None = None
|
||
used_score: int | None = None
|
||
records_total: int = 0
|
||
records_matched: int = 0
|
||
# 空串 = 本次没查兑换记录(只查积分),与 unknown(查了判不了)不同
|
||
verdict: str = ""
|
||
verdict_label: str = ""
|
||
message: str = ""
|
||
source: str = "manual"
|
||
matched_preview: list[HuyaCollabMetricMatchedOut] = Field(default_factory=list)
|
||
created_by: int | None = None
|
||
created_by_name: str = ""
|
||
created_at: datetime | None = None
|
||
|
||
|
||
class HuyaCollabFrequentGiftOut(BaseModel):
|
||
"""「常用礼包」:台账里出现过的礼包名 + 用过几次(新建/补资料时的快捷按钮)。
|
||
|
||
只有 ``name`` 与 ``used`` —— 当前活动还剩多少是礼包列表那边的事,
|
||
两边混在一起会让人以为这个数字是库存(见设计文档 §11.4 的 SID 教训)。
|
||
"""
|
||
|
||
name: str
|
||
used: int = 0
|
||
|
||
|
||
class HuyaExternalOrderActivityItemOut(BaseModel):
|
||
"""批量查询里的一行结论。"""
|
||
|
||
ok: bool = False
|
||
order_id: int
|
||
order_no: str = ""
|
||
# 本次查的是哪一半(``both`` / ``score`` / ``records``)
|
||
scope: str = ""
|
||
scope_label: str = ""
|
||
# 只查积分时为**空串**(未判定),不是 ``unknown``(查了但判不了)
|
||
verdict: str = "unknown"
|
||
verdict_label: str = ""
|
||
score: int | None = None
|
||
gained_score: int | None = None
|
||
used_score: int | None = None
|
||
records_total: int = 0
|
||
records_matched: int = 0
|
||
message: str = ""
|
||
# 查询后是否自动改了状态(空 = 没改,见服务层:只有「已上脚本」的单会自动改)
|
||
auto_status: str = ""
|
||
auto_status_label: str = ""
|
||
status: str = ""
|
||
status_label: str = ""
|
||
|
||
|
||
class HuyaExternalOrderActivityQueryOut(BaseModel):
|
||
"""批量查询汇总:``success + failed + unknown + not_judged = total``。"""
|
||
|
||
total: int
|
||
success: int
|
||
failed: int
|
||
unknown: int = 0
|
||
# 本次没查兑换记录、因此没有判定结论的行数(只查积分时全落这里)
|
||
not_judged: int = 0
|
||
auto_changed: int = 0
|
||
scope: str = ""
|
||
scope_label: str = ""
|
||
results: list[HuyaExternalOrderActivityItemOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaCollabBuyerClusterOut(BaseModel):
|
||
"""同一买家的多单(软识别,只读派生)。
|
||
|
||
``key`` 是买家指纹的可读文本(``绑定账号 · 绑定角色``);``method`` 说明识别口径,
|
||
免得以后有人以为这是人工"软绑定"关系。
|
||
"""
|
||
|
||
key: str = ""
|
||
method: str = ""
|
||
total: int = 0
|
||
others: list[HuyaCollabBuyerOrderOut] = Field(default_factory=list)
|
||
|
||
|
||
class HuyaExternalOrderOut(BaseModel):
|
||
"""一行的完整视图。
|
||
|
||
``*_image_urls`` 由后端拼(``/api/files/object?key=...``),
|
||
``*_image_thumb_urls`` 走缩略图变体(变体缺失自动回退原图)。
|
||
"""
|
||
|
||
id: int
|
||
order_no: str
|
||
shop: str
|
||
owner_id: int | None = None
|
||
owner_name: str = ""
|
||
|
||
uid: str = ""
|
||
yyuid: str = ""
|
||
hy_username: str = ""
|
||
nickname: str = ""
|
||
has_cookie: bool = False
|
||
has_raw_cookie: bool = False
|
||
ck_status: str = "unverified"
|
||
ck_status_label: str = ""
|
||
ck_checked_at: datetime | None = None
|
||
ck_check_message: str = ""
|
||
|
||
bind_douyu_account: str = ""
|
||
bind_douyu_uid: str = ""
|
||
# 探针读到的绑定对象(只读快照:账号 / 角色 / 平台·区服 + 读取时间)
|
||
bind_game_account: str = ""
|
||
bind_game_role: str = ""
|
||
bind_game_channel: str = ""
|
||
bind_snapshot_at: datetime | None = None
|
||
bind_images: list[str] = Field(default_factory=list)
|
||
home_images: list[str] = Field(default_factory=list)
|
||
bind_image_urls: list[str] = Field(default_factory=list)
|
||
home_image_urls: list[str] = Field(default_factory=list)
|
||
bind_image_thumb_urls: list[str] = Field(default_factory=list)
|
||
home_image_thumb_urls: list[str] = Field(default_factory=list)
|
||
|
||
gift_name: str = ""
|
||
required_points: int | None = None
|
||
remark: str = ""
|
||
|
||
status: str = "incomplete"
|
||
status_label: str = ""
|
||
next_status: str | None = None
|
||
bound_at: datetime | None = None
|
||
recharged_at: datetime | None = None
|
||
scripted_at: datetime | None = None
|
||
done_at: datetime | None = None
|
||
points_checked_at: datetime | None = None
|
||
points_balance: int | None = None
|
||
points_required_total: int | None = None
|
||
points_surplus: int | None = None
|
||
points_threshold: int | None = None
|
||
points_check_reason: str = ""
|
||
points_check_message: str = ""
|
||
points_check_detail: dict[str, Any] | None = None
|
||
void_reason: str = ""
|
||
|
||
# 归档是可见性状态,不影响上面的业务状态。
|
||
is_archived: bool = False
|
||
archived_at: datetime | None = None
|
||
archived_by: int | None = None
|
||
archived_by_name: str = ""
|
||
archive_reason: str = ""
|
||
|
||
points: int | None = None
|
||
elite_opened: bool | None = None
|
||
snapshot_at: datetime | None = None
|
||
|
||
created_by: int | None = None
|
||
created_by_name: str = ""
|
||
updated_by: int | None = None
|
||
updated_by_name: str = ""
|
||
version: int = 0
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
# 同一买家的其它单(软识别:绑定账号 + 绑定角色相同;只读派生,没识别到就是 null)
|
||
buyer_cluster: HuyaCollabBuyerClusterOut | None = None
|
||
|
||
# 最近一次「积分 / 兑换记录」快照(完整时间线走 GET /{id}/metrics)
|
||
# 积分与判定是**分开**取最近一条查到过它的:只查积分的那次不产生判定,反之亦然
|
||
# (2026-10-17 拆分查询入口,见 services/huya_collab_service.order_metric_views)
|
||
last_score: int | None = None
|
||
last_metric_at: datetime | None = None
|
||
last_metric_message: str = ""
|
||
last_verdict: str = ""
|
||
last_verdict_label: str = ""
|
||
last_verdict_at: datetime | None = None
|
||
last_verdict_message: str = ""
|
||
|
||
|
||
class HuyaExternalOrderListOut(BaseModel):
|
||
"""列表响应:共享视图,带上总数供分页使用。"""
|
||
|
||
items: list[HuyaExternalOrderOut]
|
||
total: int
|
||
page: int
|
||
page_size: int
|
||
|
||
|
||
class HuyaExternalOrderSummaryOut(BaseModel):
|
||
"""页头统计条(口径与当前 archive_scope / include_void 一致)。"""
|
||
|
||
total: int
|
||
by_status: dict[str, int]
|
||
ck_invalid: int
|
||
archived_total: int = 0
|
||
|
||
|
||
class HuyaExternalOrderOwnerOut(BaseModel):
|
||
"""负责人下拉项(活跃用户)。"""
|
||
|
||
id: int
|
||
username: str
|
||
|
||
|
||
class HuyaExternalOrderAuditOut(BaseModel):
|
||
"""详情页的时间线条目(直接来自 audit_logs)。"""
|
||
|
||
id: int
|
||
action: str
|
||
username: str = ""
|
||
detail: str = ""
|
||
success: bool | None = None
|
||
created_at: datetime | None = None
|
||
|
||
|
||
# ==================== 斗鱼外部单协作 ====================
|
||
|
||
|
||
class DouyuCollabShopCreate(BaseModel):
|
||
name: str = Field(..., min_length=1, max_length=64)
|
||
remark: str = Field("", max_length=255)
|
||
|
||
|
||
class DouyuCollabShopUpdate(BaseModel):
|
||
name: str | None = Field(None, min_length=1, max_length=64)
|
||
sort_order: int | None = None
|
||
is_active: bool | None = None
|
||
remark: str | None = Field(None, max_length=255)
|
||
|
||
|
||
class DouyuCollabShopOut(BaseModel):
|
||
id: int
|
||
name: str
|
||
sort_order: int = 0
|
||
is_active: bool = True
|
||
remark: str = ""
|
||
created_by: int | None = None
|
||
updated_by: int | None = None
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
|
||
class DouyuCollabGoodsOut(BaseModel):
|
||
commodity_id: str = ""
|
||
name: str = ""
|
||
score: int | None = None
|
||
status: str = ""
|
||
open_status: int | None = None
|
||
start_time: int | None = None
|
||
end_time: int | None = None
|
||
mark: str = ""
|
||
count: int | None = None
|
||
elite_limited: int | None = None
|
||
is_limited: bool = False
|
||
raw: dict[str, Any] | None = None
|
||
|
||
|
||
class DouyuCollabAccountLookupRequest(BaseModel):
|
||
username: str = Field(..., min_length=1, max_length=128)
|
||
with_goods: bool = True
|
||
|
||
|
||
class DouyuCollabAccountLookupOut(BaseModel):
|
||
found: bool = False
|
||
ok: bool = False
|
||
message: str = ""
|
||
errors: list[str] = Field(default_factory=list)
|
||
account_id: int | None = None
|
||
account_username: str = ""
|
||
account_uid: str = ""
|
||
account_nickname: str = ""
|
||
ck_status: str = ""
|
||
ck_status_label: str = ""
|
||
manual_type: int | None = None
|
||
manual_score: int | None = None
|
||
expire_time: int | None = None
|
||
manual_opened: bool = False
|
||
bind_game_account: str = ""
|
||
bind_game_role: str = ""
|
||
bind_game_channel: str = ""
|
||
fish_fin_balance: int | None = None
|
||
goods: list[DouyuCollabGoodsOut] = Field(default_factory=list)
|
||
goods_count: int = 0
|
||
orders_using_account: list[dict[str, Any]] = Field(default_factory=list)
|
||
|
||
|
||
class DouyuCollabOrderCreate(BaseModel):
|
||
order_no: str = Field(..., min_length=1, max_length=64)
|
||
shop: str = Field("", max_length=64)
|
||
owner_id: int | None = None
|
||
account_username: str = Field("", max_length=128)
|
||
gift_name: str = Field("", max_length=128)
|
||
commodity_id: str = Field("", max_length=64)
|
||
required_points: int | None = Field(None, ge=0)
|
||
home_images: list[str] = Field(default_factory=list)
|
||
remark: str = Field("", max_length=512)
|
||
draft: bool = False
|
||
require_verified: bool = True
|
||
|
||
|
||
class DouyuCollabOrderUpdate(BaseModel):
|
||
order_no: str | None = Field(None, max_length=64)
|
||
shop: str | None = Field(None, max_length=64)
|
||
owner_id: int | None = None
|
||
account_username: str | None = Field(None, max_length=128)
|
||
gift_name: str | None = Field(None, max_length=128)
|
||
commodity_id: str | None = Field(None, max_length=64)
|
||
required_points: int | None = Field(None, ge=0)
|
||
home_images: list[str] | None = None
|
||
remark: str | None = Field(None, max_length=512)
|
||
expected_version: int | None = None
|
||
|
||
|
||
class DouyuCollabStatusRequest(BaseModel):
|
||
status: str = Field(..., min_length=1, max_length=16)
|
||
reason: str = Field("", max_length=256)
|
||
rollback: bool = False
|
||
expected_version: int | None = None
|
||
|
||
|
||
class DouyuCollabStatusBatchRequest(BaseModel):
|
||
ids: list[int] = Field(default_factory=list)
|
||
status: str = Field(..., min_length=1, max_length=16)
|
||
reason: str = Field("", max_length=256)
|
||
rollback: bool = False
|
||
|
||
|
||
class DouyuCollabAccountRequest(BaseModel):
|
||
account_username: str = Field(..., min_length=1, max_length=128)
|
||
expected_version: int | None = None
|
||
|
||
|
||
class DouyuCollabImagesRequest(BaseModel):
|
||
home_images: list[str] = Field(default_factory=list)
|
||
expected_version: int | None = None
|
||
|
||
|
||
class DouyuCollabArchiveRequest(BaseModel):
|
||
ids: list[int] = Field(default_factory=list)
|
||
reason: str = Field("", max_length=256)
|
||
|
||
|
||
class DouyuCollabCkExportRequest(BaseModel):
|
||
"""勾选多行,导出关联内部账号的 CK 明文(一次最多 50 条)。"""
|
||
|
||
ids: list[int] = Field(default_factory=list)
|
||
|
||
|
||
class DouyuCollabCkExportItemOut(BaseModel):
|
||
"""导出结果里的一行:``ck_text`` 是明文凭据,只走这个接口、不留日志。"""
|
||
|
||
id: int
|
||
order_no: str = ""
|
||
account_id: int | None = None
|
||
account_username: str = ""
|
||
account_nickname: str = ""
|
||
status: str = ""
|
||
status_label: str = ""
|
||
ck_status: str = ""
|
||
ck_status_label: str = ""
|
||
ck_text: str = ""
|
||
|
||
|
||
class DouyuCollabCkExportSkippedOut(BaseModel):
|
||
id: int
|
||
order_no: str = ""
|
||
reason: str = ""
|
||
|
||
|
||
class DouyuCollabCkExportOut(BaseModel):
|
||
"""导出 CK 的结果:``exported + len(skipped) = total``。"""
|
||
|
||
total: int
|
||
exported: int
|
||
skipped: list[DouyuCollabCkExportSkippedOut] = Field(default_factory=list)
|
||
items: list[DouyuCollabCkExportItemOut] = Field(default_factory=list)
|
||
|
||
|
||
class DouyuCollabOrderOut(BaseModel):
|
||
id: int
|
||
order_no: str
|
||
shop: str = ""
|
||
owner_id: int | None = None
|
||
owner_name: str = ""
|
||
|
||
account_id: int | None = None
|
||
account_username: str = ""
|
||
account_uid: str = ""
|
||
account_nickname: str = ""
|
||
account_exists: bool = False
|
||
has_cookie: bool = False
|
||
account_ck_status: str = ""
|
||
account_status: str = ""
|
||
|
||
bind_game_account: str = ""
|
||
bind_game_role: str = ""
|
||
bind_game_channel: str = ""
|
||
bind_snapshot_at: datetime | None = None
|
||
|
||
home_images: list[str] = Field(default_factory=list)
|
||
home_image_urls: list[str] = Field(default_factory=list)
|
||
home_image_thumb_urls: list[str] = Field(default_factory=list)
|
||
gift_name: str = ""
|
||
commodity_id: str = ""
|
||
required_points: int | None = None
|
||
remark: str = ""
|
||
|
||
status: str = "incomplete"
|
||
status_label: str = ""
|
||
next_status: str | None = None
|
||
missing_fields: list[str] = Field(default_factory=list)
|
||
fish_fin_balance: int | None = None
|
||
points_balance: int | None = None
|
||
points_required_total: int | None = None
|
||
points_surplus: int | None = None
|
||
points_threshold: int | None = None
|
||
points_checked_at: datetime | None = None
|
||
points_check_reason: str = ""
|
||
points_check_message: str = ""
|
||
points_check_detail: dict[str, Any] | None = None
|
||
recharged_at: datetime | None = None
|
||
scripted_at: datetime | None = None
|
||
done_at: datetime | None = None
|
||
void_reason: str = ""
|
||
|
||
is_archived: bool = False
|
||
archived_at: datetime | None = None
|
||
archived_by: int | None = None
|
||
archived_by_name: str = ""
|
||
archive_reason: str = ""
|
||
|
||
created_by: int | None = None
|
||
created_by_name: str = ""
|
||
updated_by: int | None = None
|
||
updated_by_name: str = ""
|
||
version: int = 0
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
account_cluster: dict[str, Any] | None = None
|
||
|
||
last_metric_at: datetime | None = None
|
||
last_metric_message: str = ""
|
||
last_verdict: str = ""
|
||
last_verdict_label: str = ""
|
||
last_records_total: int = 0
|
||
last_records_matched: int = 0
|
||
|
||
|
||
class DouyuCollabOrderListOut(BaseModel):
|
||
items: list[DouyuCollabOrderOut]
|
||
total: int
|
||
page: int
|
||
page_size: int
|
||
|
||
|
||
class DouyuCollabSummaryOut(BaseModel):
|
||
total: int
|
||
by_status: dict[str, int]
|
||
account_missing: int = 0
|
||
ck_invalid: int = 0
|
||
archived_total: int = 0
|
||
|
||
|
||
class DouyuCollabOwnerOut(BaseModel):
|
||
id: int
|
||
username: str
|
||
|
||
|
||
class DouyuCollabMetricOut(BaseModel):
|
||
id: int
|
||
order_id: int
|
||
order_no: str = ""
|
||
account_username: str = ""
|
||
fish_fin_balance: int | None = None
|
||
points_balance: int | None = None
|
||
points_required_total: int | None = None
|
||
points_surplus: int | None = None
|
||
records_total: int = 0
|
||
records_matched: int = 0
|
||
verdict: str = ""
|
||
verdict_label: str = ""
|
||
message: str = ""
|
||
source: str = ""
|
||
created_at: datetime | None = None
|
||
|
||
|
||
class DouyuCollabTimelineOut(BaseModel):
|
||
id: int
|
||
action: str
|
||
username: str = ""
|
||
detail: str = ""
|
||
success: bool | None = None
|
||
created_at: datetime | None = None
|