type: 收窄测试与响应模型类型

This commit is contained in:
yml2213
2026-08-30 20:21:14 +08:00
parent 3f97dbe525
commit cbd314f4e5
7 changed files with 96 additions and 69 deletions
+4 -2
View File
@@ -35,7 +35,7 @@ class TestAccountSensitiveFields:
def test_sensitive_fields_are_hidden_by_default_even_for_admin(self): def test_sensitive_fields_are_hidden_by_default_even_for_admin(self):
result = list_accounts( result = list_accounts(
assigned_only=False, assigned_only=False,
tag=None, tag="",
has_cookie=False, has_cookie=False,
search="", search="",
page=1, page=1,
@@ -45,6 +45,7 @@ class TestAccountSensitiveFields:
current=self.admin, current=self.admin,
) )
assert isinstance(result, dict)
item = result["items"][0] item = result["items"][0]
assert item.password is None assert item.password is None
assert item.email is None assert item.email is None
@@ -53,7 +54,7 @@ class TestAccountSensitiveFields:
def test_admin_can_explicitly_request_sensitive_fields(self): def test_admin_can_explicitly_request_sensitive_fields(self):
result = list_accounts( result = list_accounts(
assigned_only=False, assigned_only=False,
tag=None, tag="",
has_cookie=False, has_cookie=False,
search="", search="",
page=1, page=1,
@@ -63,6 +64,7 @@ class TestAccountSensitiveFields:
current=self.admin, current=self.admin,
) )
assert isinstance(result, dict)
item = result["items"][0] item = result["items"][0]
assert item.password == "account-password" assert item.password == "account-password"
assert item.email == "account@example.com" assert item.email == "account@example.com"
+5 -3
View File
@@ -1,12 +1,13 @@
import pytest import pytest
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import patch
from typing import cast
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from web.backend.database import Base from web.backend.database import Base
from web.backend.models import Account, LoginTask, User from web.backend.models import Account, LoginTask, ProxyConfig, User
from web.backend.services.login_service import ( from web.backend.services.login_service import (
LoginBatchRunner, LoginBatchRunner,
cleanup_orphan_relogin_tasks, cleanup_orphan_relogin_tasks,
@@ -137,6 +138,7 @@ class TestCookieOperation:
current=admin, current=admin,
) )
assert isinstance(result, dict)
assert result["total"] == 1 assert result["total"] == 1
assert [item["id"] for item in result["items"]] == [self.owned_task.id] assert [item["id"] for item in result["items"]] == [self.owned_task.id]
@@ -200,7 +202,7 @@ class TestCookieOperation:
page=1, page=1,
page_size=20, page_size=20,
db=self.session, db=self.session,
current=no_permission_user, current=cast(User, no_permission_user),
) )
def test_relogin_failed_record_stays_visible_and_can_be_checked(self): def test_relogin_failed_record_stays_visible_and_can_be_checked(self):
@@ -235,7 +237,7 @@ class TestCookieOperation:
assert self.other_task.status == "success" assert self.other_task.status == "success"
def test_runner_copies_proxy_config_before_background_execution(self): def test_runner_copies_proxy_config_before_background_execution(self):
proxy = SimpleNamespace( proxy = ProxyConfig(
enabled=True, enabled=True,
http="http://127.0.0.1:8080", http="http://127.0.0.1:8080",
https="", https="",
+21 -16
View File
@@ -94,16 +94,18 @@ class TestDouyuGoldRechargeChannel:
assert str(supplier.create_order.call_args.kwargs["pay_amount"]) == "10" assert str(supplier.create_order.call_args.kwargs["pay_amount"]) == "10"
self.session.refresh(self.task) self.session.refresh(self.task)
assert self.task.status == "success" assert self.task.status == "success"
assert self.task.result["recharge_channel"] == "supplier_api" task_result = self.task.result
assert self.task.result["supplier_order_status"] == 2 assert isinstance(task_result, dict)
assert self.task.result["buy_num"] == 10 assert task_result["recharge_channel"] == "supplier_api"
assert self.task.result["out_order_id"] == expected_order_id assert task_result["supplier_order_status"] == 2
assert self.task.result["recharge_account"] == "罗炅729" assert task_result["buy_num"] == 10
assert self.task.result["douyu_uid"] == "10001" assert task_result["out_order_id"] == expected_order_id
assert self.task.result["order_id"] == "supplier-001" assert task_result["recharge_account"] == "罗炅729"
assert self.task.result["pay_amount"] == "10" assert task_result["douyu_uid"] == "10001"
assert "pay_url" not in self.task.result assert task_result["order_id"] == "supplier-001"
assert "sign" not in self.task.result["supplier_order"] assert task_result["pay_amount"] == "10"
assert "pay_url" not in task_result
assert "sign" not in task_result["supplier_order"]
@patch("web.backend.services.douyu_runner_gold.FishFinRechargeClient") @patch("web.backend.services.douyu_runner_gold.FishFinRechargeClient")
def test_supplier_channel_rejects_account_without_nickname(self, client_class): def test_supplier_channel_rejects_account_without_nickname(self, client_class):
@@ -226,13 +228,14 @@ class TestDouyuGoldRechargeChannel:
try: try:
task = verify_session.get(DouyuTask, task_id) task = verify_session.get(DouyuTask, task_id)
checked_login = verify_session.get(LoginTask, login_task_id) checked_login = verify_session.get(LoginTask, login_task_id)
assert task is not None
assert checked_login is not None
assert task.status == "failed" assert task.status == "failed"
assert "Cookie 已失效,请重新登录" in task.message assert "Cookie 已失效,请重新登录" in task.message
assert checked_login.ck_check_status == "invalid" assert checked_login.ck_check_status == "invalid"
assert ( check_result = checked_login.ck_check_result
checked_login.ck_check_result["message"] assert isinstance(check_result, dict)
== "鱼丸接口: 未登录;等级接口: 未登录" assert check_result["message"] == "鱼丸接口: 未登录;等级接口: 未登录"
)
finally: finally:
verify_session.close() verify_session.close()
@@ -273,8 +276,10 @@ class TestDouyuGoldRechargeChannel:
assert self.task.message == "供应商直充成功(异步通知)" assert self.task.message == "供应商直充成功(异步通知)"
self.session.refresh(self.account) self.session.refresh(self.account)
assert self.account.bind_status == "gold_recharged" assert self.account.bind_status == "gold_recharged"
assert self.task.result["order_id"] == "supplier-001" task_result = self.task.result
assert self.task.result["supplier_order_status"] == 2 assert isinstance(task_result, dict)
assert task_result["order_id"] == "supplier-001"
assert task_result["supplier_order_status"] == 2
audit = ( audit = (
self.session.query(AuditLog) self.session.query(AuditLog)
.filter(AuditLog.action == "recharge:douyu:callback") .filter(AuditLog.action == "recharge:douyu:callback")
+10 -8
View File
@@ -1,5 +1,6 @@
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import Mock from unittest.mock import Mock
from typing import Any, cast
from core.douyu.login import DouyuLogin from core.douyu.login import DouyuLogin
from core.douyu.login_api_wgapi import WgapiLoginAPI from core.douyu.login_api_wgapi import WgapiLoginAPI
@@ -9,13 +10,14 @@ class TestMobileBindSkip:
def setup_method(self): def setup_method(self):
self.login = DouyuLogin.__new__(DouyuLogin) self.login = DouyuLogin.__new__(DouyuLogin)
self.login.api = WgapiLoginAPI() self.login.api = WgapiLoginAPI()
self.login.account = SimpleNamespace( self.login.account = cast(
username="test-user", password="test-password" Any, SimpleNamespace(username="test-user", password="test-password")
) )
self.login._request_json = Mock() self.request_json = Mock()
self.login.__dict__["_request_json"] = self.request_json
def test_second_login_skips_mobile_bind_with_unique_key(self): def test_second_login_skips_mobile_bind_with_unique_key(self):
self.login._request_json.side_effect = [ self.request_json.side_effect = [
{ {
"error": 130014, "error": 130014,
"msg": "需要进行手机号绑定", "msg": "需要进行手机号绑定",
@@ -41,8 +43,8 @@ class TestMobileBindSkip:
assert ( assert (
login_url == "https://www.douyu.com/api/passport/login?code=callback-code" login_url == "https://www.douyu.com/api/passport/login?code=callback-code"
) )
assert self.login._request_json.call_count == 2 assert self.request_json.call_count == 2
skip_call = self.login._request_json.call_args_list[1] skip_call = self.request_json.call_args_list[1]
assert skip_call.args[:3] == ( assert skip_call.args[:3] == (
"post", "post",
"https://passport.douyu.com/wgapi/member/passport/login", "https://passport.douyu.com/wgapi/member/passport/login",
@@ -55,7 +57,7 @@ class TestMobileBindSkip:
} }
def test_second_login_preserves_remote_email_flow_without_unique_key(self): def test_second_login_preserves_remote_email_flow_without_unique_key(self):
self.login._request_json.return_value = { self.request_json.return_value = {
"error": 130014, "error": 130014,
"data": {"remoteLogin": {"code": "remote-code"}}, "data": {"remoteLogin": {"code": "remote-code"}},
} }
@@ -69,4 +71,4 @@ class TestMobileBindSkip:
) )
assert (next_step, remote_code) == ("remote_email", "remote-code") assert (next_step, remote_code) == ("remote_email", "remote-code")
assert self.login._request_json.call_count == 1 assert self.request_json.call_count == 1
+10 -17
View File
@@ -1,5 +1,4 @@
import pytest import pytest
from types import SimpleNamespace
from sqlalchemy import create_engine, event from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -54,12 +53,6 @@ class TestDouyuWorkbenchScope:
) )
) )
self.session.commit() self.session.commit()
self.current = SimpleNamespace(
id=self.user.id,
username=self.user.username,
role=self.user.role,
custom_permissions=None,
)
def teardown_method(self): def teardown_method(self):
self.session.close() self.session.close()
@@ -72,12 +65,10 @@ class TestDouyuWorkbenchScope:
handbook_scope="elite", account_ids=[self.account.id] handbook_scope="elite", account_ids=[self.account.id]
), ),
db=self.session, db=self.session,
current=self.current, current=self.user,
)
elite = list_workbench_accounts("elite", db=self.session, current=self.current)
esports = list_workbench_accounts(
"esports", db=self.session, current=self.current
) )
elite = list_workbench_accounts("elite", db=self.session, current=self.user)
esports = list_workbench_accounts("esports", db=self.session, current=self.user)
assert elite["account_ids"] == [self.account.id] assert elite["account_ids"] == [self.account.id]
assert esports["account_ids"] == [] assert esports["account_ids"] == []
assert elite["configured"] assert elite["configured"]
@@ -87,10 +78,10 @@ class TestDouyuWorkbenchScope:
update_workbench_accounts( update_workbench_accounts(
DouyuWorkbenchAccountsUpdate(handbook_scope="elite", account_ids=[]), DouyuWorkbenchAccountsUpdate(handbook_scope="elite", account_ids=[]),
db=self.session, db=self.session,
current=self.current, current=self.user,
) )
empty_elite = list_workbench_accounts( empty_elite = list_workbench_accounts(
"elite", db=self.session, current=self.current "elite", db=self.session, current=self.user
) )
assert empty_elite["account_ids"] == [] assert empty_elite["account_ids"] == []
assert empty_elite["configured"] assert empty_elite["configured"]
@@ -118,7 +109,7 @@ class TestDouyuWorkbenchScope:
page=None, page=None,
page_size=100, page_size=100,
db=self.session, db=self.session,
current=self.current, current=self.user,
) )
esports = list_tasks( esports = list_tasks(
batch_id=None, batch_id=None,
@@ -127,8 +118,10 @@ class TestDouyuWorkbenchScope:
page=None, page=None,
page_size=100, page_size=100,
db=self.session, db=self.session,
current=self.current, current=self.user,
) )
assert isinstance(elite, list)
assert isinstance(esports, list)
assert {item.batch_id for item in elite} == {elite_batch} assert {item.batch_id for item in elite} == {elite_batch}
assert {item.batch_id for item in esports} == {esports_batch} assert {item.batch_id for item in esports} == {esports_batch}
assert { assert {
@@ -182,6 +175,6 @@ class TestDouyuWorkbenchScope:
) )
self.session.commit() self.session.commit()
delete_account(self.account.id, db=self.session, current=self.current) delete_account(self.account.id, db=self.session, current=self.user)
assert self.session.query(DouyuWorkbenchAccount).count() == 0 assert self.session.query(DouyuWorkbenchAccount).count() == 0
+5 -5
View File
@@ -1,6 +1,4 @@
import pytest import pytest
from types import SimpleNamespace
from sqlalchemy import create_engine, event from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -53,11 +51,12 @@ class TestUserDeletion:
result = delete_user( result = delete_user(
self.user.id, self.user.id,
db=self.session, db=self.session,
current=SimpleNamespace(id=self.admin.id, username=self.admin.username), current=self.admin,
) )
deleted = self.session.get(User, self.user.id) deleted = self.session.get(User, self.user.id)
account = self.session.query(Account).one() account = self.session.query(Account).one()
assert deleted is not None
assert result["success"] assert result["success"]
assert result["released_accounts"] == 1 assert result["released_accounts"] == 1
assert not deleted.is_active assert not deleted.is_active
@@ -74,7 +73,7 @@ class TestUserDeletion:
delete_user( delete_user(
self.user.id, self.user.id,
db=self.session, db=self.session,
current=SimpleNamespace(id=self.user.id, username=self.user.username), current=self.user,
) )
def test_rename_preserves_user_id_and_related_data(self): def test_rename_preserves_user_id_and_related_data(self):
@@ -82,12 +81,13 @@ class TestUserDeletion:
self.user.id, self.user.id,
UserRename(username="renamed-support"), UserRename(username="renamed-support"),
db=self.session, db=self.session,
current=SimpleNamespace(id=self.admin.id, username=self.admin.username), current=self.admin,
) )
renamed = self.session.get(User, self.user.id) renamed = self.session.get(User, self.user.id)
account = self.session.query(Account).one() account = self.session.query(Account).one()
task = self.session.query(LoginTask).one() task = self.session.query(LoginTask).one()
assert renamed is not None
assert result.username == "renamed-support" assert result.username == "renamed-support"
assert renamed.id == self.user.id assert renamed.id == self.user.id
assert account.assigned_to == self.user.id assert account.assigned_to == self.user.id
+41 -18
View File
@@ -41,6 +41,11 @@ def _ensure_tz(dt: Optional[datetime]) -> Optional[datetime]:
return dt return dt
def _isoformat(dt: Optional[datetime]) -> str | None:
normalized = _ensure_tz(dt)
return normalized.isoformat() if normalized is not None else None
# ---- 认证 ---- # ---- 认证 ----
class LoginRequest(BaseModel): class LoginRequest(BaseModel):
username: str username: str
@@ -75,7 +80,7 @@ class UserInfo(BaseModel):
"role": self.role, "role": self.role,
"is_active": self.is_active, "is_active": self.is_active,
"remark": self.remark, "remark": self.remark,
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "created_at": _isoformat(self.created_at),
"permissions": self.permissions, "permissions": self.permissions,
"custom_permissions": self.custom_permissions, "custom_permissions": self.custom_permissions,
} }
@@ -108,6 +113,7 @@ class AccountImport(BaseModel):
tag 为本次导入的统一标签兜底:某行未单独写标签时使用该值, tag 为本次导入的统一标签兜底:某行未单独写标签时使用该值,
行内第5列标签优先。 行内第5列标签优先。
""" """
text: str text: str
tag: str = "" tag: str = ""
@@ -123,6 +129,7 @@ class BatchAssign(BaseModel):
class AccountBulkSelection(BaseModel): class AccountBulkSelection(BaseModel):
"""批量操作选择范围:显式 ID 或当前筛选条件下全部账号。""" """批量操作选择范围:显式 ID 或当前筛选条件下全部账号。"""
account_ids: list[int] = Field(default_factory=list) account_ids: list[int] = Field(default_factory=list)
all_matching: bool = False all_matching: bool = False
search: str = "" search: str = ""
@@ -167,7 +174,7 @@ class AccountOut(BaseModel):
"assigned_to": self.assigned_to, "assigned_to": self.assigned_to,
"assigned_username": self.assigned_username, "assigned_username": self.assigned_username,
"remark": self.remark, "remark": self.remark,
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "created_at": _isoformat(self.created_at),
} }
@@ -178,7 +185,9 @@ class LoginBatchRequest(BaseModel):
max_total_time: float = 300 # 单账号登录总时长上限(秒),0=不限制 max_total_time: float = 300 # 单账号登录总时长上限(秒),0=不限制
concurrency: int = 3 # 并发数,1-10 concurrency: int = 3 # 并发数,1-10
api_strategy: str = "wgapi" # 接口策略: wgapi(新版)或 iframe(旧版备选) api_strategy: str = "wgapi" # 接口策略: wgapi(新版)或 iframe(旧版备选)
mode: str = Field("login", pattern="^(login|check)$") # login=登录取CKcheck=账号状态检测 mode: str = Field(
"login", pattern="^(login|check)$"
) # login=登录取CKcheck=账号状态检测
class CookieReloginRequest(BaseModel): class CookieReloginRequest(BaseModel):
@@ -210,8 +219,8 @@ class LoginTaskOut(BaseModel):
"cookie": self.cookie, "cookie": self.cookie,
"message": self.message, "message": self.message,
"created_by": self.created_by, "created_by": self.created_by,
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "created_at": _isoformat(self.created_at),
"finished_at": _ensure_tz(self.finished_at).isoformat() if self.finished_at else None, "finished_at": _isoformat(self.finished_at),
} }
@@ -256,12 +265,14 @@ class AccountCheckBatchOut(BaseModel):
# ---- 虎牙 ---- # ---- 虎牙 ----
class HuyaCookieImport(BaseModel): class HuyaCookieImport(BaseModel):
"""批量导入虎牙 Cookie。支持纯 CK 或 账号----密码----CK。""" """批量导入虎牙 Cookie。支持纯 CK 或 账号----密码----CK。"""
text: str text: str
tag: str = "" tag: str = ""
class HuyaPasswordLoginRequest(BaseModel): class HuyaPasswordLoginRequest(BaseModel):
"""虎牙账号 Web 密码登录并保存 Cookie(旧版)。""" """虎牙账号 Web 密码登录并保存 Cookie(旧版)。"""
username: str = Field(..., min_length=1, max_length=128) username: str = Field(..., min_length=1, max_length=128)
password: str = Field(..., min_length=1, max_length=128) password: str = Field(..., min_length=1, max_length=128)
tag: str = "" tag: str = ""
@@ -270,6 +281,7 @@ class HuyaPasswordLoginRequest(BaseModel):
class HuyaAppPasswordLoginRequest(BaseModel): class HuyaAppPasswordLoginRequest(BaseModel):
"""虎牙账号 App 协议密码登录并保存 Cookie(推荐)。""" """虎牙账号 App 协议密码登录并保存 Cookie(推荐)。"""
username: str = Field(..., min_length=1, max_length=128) username: str = Field(..., min_length=1, max_length=128)
password: str = Field(..., min_length=1, max_length=128) password: str = Field(..., min_length=1, max_length=128)
tag: str = "" tag: str = ""
@@ -278,12 +290,14 @@ class HuyaAppPasswordLoginRequest(BaseModel):
class HuyaSmsCodeRequest(BaseModel): class HuyaSmsCodeRequest(BaseModel):
"""发送虎牙短信验证码。""" """发送虎牙短信验证码。"""
phone: str = Field(..., min_length=5, max_length=32) phone: str = Field(..., min_length=5, max_length=32)
cookie: str = "" cookie: str = ""
class HuyaSmsLoginRequest(BaseModel): class HuyaSmsLoginRequest(BaseModel):
"""提交虎牙短信验证码并保存 Cookie。""" """提交虎牙短信验证码并保存 Cookie。"""
authcode: str = Field(..., min_length=4, max_length=8) authcode: str = Field(..., min_length=4, max_length=8)
state: str = Field(..., min_length=1) state: str = Field(..., min_length=1)
phone: str = "" phone: str = ""
@@ -292,6 +306,7 @@ class HuyaSmsLoginRequest(BaseModel):
class HuyaAutoRegisterRequest(BaseModel): class HuyaAutoRegisterRequest(BaseModel):
"""虎牙自动注册批次。每行格式:手机号----短信查询URL。""" """虎牙自动注册批次。每行格式:手机号----短信查询URL。"""
text: str = Field(..., min_length=1) text: str = Field(..., min_length=1)
tag: str = "" tag: str = ""
concurrency: int = Field(1, ge=1, le=5) concurrency: int = Field(1, ge=1, le=5)
@@ -310,6 +325,7 @@ class HuyaAutoRegisterRetryRequest(BaseModel):
- retry_failed: 只重试 error - retry_failed: 只重试 error
- all_unfinished: 失败+未完成都跑 - all_unfinished: 失败+未完成都跑
""" """
mode: str = Field("continue", max_length=32) mode: str = Field("continue", max_length=32)
concurrency: Optional[int] = Field(None, ge=1, le=5) concurrency: Optional[int] = Field(None, ge=1, le=5)
wait_seconds: Optional[float] = Field(None, ge=15, le=600) wait_seconds: Optional[float] = Field(None, ge=15, le=600)
@@ -382,12 +398,14 @@ class HuyaRegisterSuccessLogOut(BaseModel):
class HuyaPasswordAccountImport(BaseModel): class HuyaPasswordAccountImport(BaseModel):
"""导入虎牙账号密码,稍后再选择登录。""" """导入虎牙账号密码,稍后再选择登录。"""
text: str = Field(..., min_length=1) text: str = Field(..., min_length=1)
tag: str = "" tag: str = ""
class HuyaPasswordLoginSelectedRequest(BaseModel): class HuyaPasswordLoginSelectedRequest(BaseModel):
"""选择已导入的虎牙账号执行密码登录。""" """选择已导入的虎牙账号执行密码登录。"""
account_ids: list[int] = Field(default_factory=list) account_ids: list[int] = Field(default_factory=list)
all_matching: bool = False all_matching: bool = False
search: str = "" search: str = ""
@@ -399,6 +417,7 @@ class HuyaPasswordLoginSelectedRequest(BaseModel):
class HuyaDeviceBindingsBatchDeleteRequest(BaseModel): class HuyaDeviceBindingsBatchDeleteRequest(BaseModel):
"""批量解绑设备:删除账号画像与 hydevice 指纹状态。""" """批量解绑设备:删除账号画像与 hydevice 指纹状态。"""
accounts: list[str] = Field(..., min_length=1) accounts: list[str] = Field(..., min_length=1)
@@ -447,8 +466,8 @@ class HuyaAccountOut(BaseModel):
"game_phone": self.game_phone, "game_phone": self.game_phone,
"assigned_to": self.assigned_to, "assigned_to": self.assigned_to,
"assigned_username": self.assigned_username, "assigned_username": self.assigned_username,
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "created_at": _isoformat(self.created_at),
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, "updated_at": _isoformat(self.updated_at),
} }
@@ -468,7 +487,7 @@ class HuyaConfigOut(BaseModel):
"outer_act_id": self.outer_act_id, "outer_act_id": self.outer_act_id,
"bind_act_id": self.bind_act_id, "bind_act_id": self.bind_act_id,
"pay_channel": self.pay_channel, "pay_channel": self.pay_channel,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, "updated_at": _isoformat(self.updated_at),
} }
@@ -516,8 +535,8 @@ class HuyaTaskOut(BaseModel):
"message": self.message, "message": self.message,
"result": self.result, "result": self.result,
"created_by": self.created_by, "created_by": self.created_by,
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "created_at": _isoformat(self.created_at),
"finished_at": _ensure_tz(self.finished_at).isoformat() if self.finished_at else None, "finished_at": _isoformat(self.finished_at),
} }
@@ -541,7 +560,7 @@ class HuyaGoodsOut(BaseModel):
"price": self.price, "price": self.price,
"remain_text": self.remain_text, "remain_text": self.remain_text,
"raw": self.raw, "raw": self.raw,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, "updated_at": _isoformat(self.updated_at),
} }
@@ -577,7 +596,7 @@ class HuyaRechargeGoodsOut(BaseModel):
"task_id": self.task_id, "task_id": self.task_id,
"task_name": self.task_name, "task_name": self.task_name,
"raw": self.raw, "raw": self.raw,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, "updated_at": _isoformat(self.updated_at),
} }
@@ -634,7 +653,7 @@ class DouyuConfigOut(BaseModel):
"gold_api_account_template_name": self.gold_api_account_template_name, "gold_api_account_template_name": self.gold_api_account_template_name,
"gift_id": self.gift_id, "gift_id": self.gift_id,
"skin_id": self.skin_id, "skin_id": self.skin_id,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, "updated_at": _isoformat(self.updated_at),
} }
@@ -657,7 +676,9 @@ class DouyuConfigUpdate(BaseModel):
xpd_act_id: Optional[str] = None xpd_act_id: Optional[str] = None
xpd_rid: Optional[str] = None xpd_rid: Optional[str] = None
gold_pay_type: Optional[int] = Field(None, ge=1, le=9) gold_pay_type: Optional[int] = Field(None, ge=1, le=9)
gold_recharge_channel: Optional[str] = Field(None, pattern="^(wechat_qr|supplier_api)$") gold_recharge_channel: Optional[str] = Field(
None, pattern="^(wechat_qr|supplier_api)$"
)
gold_api_product_id: Optional[str] = Field(None, max_length=128) gold_api_product_id: Optional[str] = Field(None, max_length=128)
gold_api_account_template_name: Optional[str] = Field(None, max_length=64) gold_api_account_template_name: Optional[str] = Field(None, max_length=64)
gift_id: Optional[str] = None gift_id: Optional[str] = None
@@ -710,8 +731,8 @@ class DouyuTaskOut(BaseModel):
"message": self.message, "message": self.message,
"result": self.result, "result": self.result,
"created_by": self.created_by, "created_by": self.created_by,
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None, "created_at": _isoformat(self.created_at),
"finished_at": _ensure_tz(self.finished_at).isoformat() if self.finished_at else None, "finished_at": _isoformat(self.finished_at),
} }
@@ -735,7 +756,7 @@ class DouyuGoodsOut(BaseModel):
"score": self.score, "score": self.score,
"status": self.status, "status": self.status,
"raw": self.raw, "raw": self.raw,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, "updated_at": _isoformat(self.updated_at),
} }
@@ -794,7 +815,7 @@ class DouyuXpdGoodsOut(BaseModel):
"category": self.category, "category": self.category,
"goods_left": self.goods_left, "goods_left": self.goods_left,
"raw": self.raw, "raw": self.raw,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None, "updated_at": _isoformat(self.updated_at),
} }
@@ -828,6 +849,7 @@ class ProxyConfigUpdate(BaseModel):
# ---- 代理平台元信息 ---- # ---- 代理平台元信息 ----
class PlatformFieldDef(BaseModel): class PlatformFieldDef(BaseModel):
"""平台凭据字段定义。""" """平台凭据字段定义。"""
key: str key: str
label: str label: str
placeholder: str = "" placeholder: str = ""
@@ -835,6 +857,7 @@ class PlatformFieldDef(BaseModel):
class PlatformInfo(BaseModel): class PlatformInfo(BaseModel):
"""平台元信息。""" """平台元信息。"""
name: str name: str
label: str label: str
credential_fields: list[PlatformFieldDef] credential_fields: list[PlatformFieldDef]