"""接口请求与响应。金额在 JSON 里用字符串,避免浮点误差。""" from __future__ import annotations from datetime import date, datetime from decimal import Decimal from typing import Annotated from pydantic import BaseModel, ConfigDict, Field, PlainSerializer from app.models import DatacenterStatus, NodeStatus, OrderStatus, TaskStatus, UserRole, WithdrawalStatus, YieldStatus SN = Annotated[str, Field(min_length=4, max_length=64, pattern=r"^[A-Za-z0-9_-]+$")] WALLET = Annotated[str, Field(min_length=4, max_length=128, pattern=r"^[A-Za-z0-9:_-]+$")] USERNAME = Annotated[str, Field(min_length=3, max_length=32, pattern=r"^[A-Za-z0-9_]+$")] MoneyText = Annotated[Decimal, PlainSerializer(lambda value: format(value, "f"), return_type=str)] class APIModel(BaseModel): model_config = ConfigDict(from_attributes=True) class UserCreate(APIModel): wallet_address: WALLET class UserOut(APIModel): id: int wallet_address: str username: str | None = None role: UserRole = UserRole.USER created_at: datetime class RegisterIn(APIModel): wallet_address: WALLET username: USERNAME | None = None password: str | None = Field(default=None, max_length=72) class LoginIn(APIModel): wallet_address: str | None = Field(default=None, max_length=128) username: str | None = Field(default=None, max_length=32) password: str | None = Field(default=None, max_length=72) class TokenOut(APIModel): access_token: str token_type: str = "bearer" role: UserRole wallet_address: str username: str | None = None class AdminUserOut(APIModel): id: int wallet_address: str username: str | None role: UserRole created_at: datetime node_count: int available_points: MoneyText class NodeCreate(APIModel): wallet_address: WALLET sn: SN purchase_amount: Decimal = Field(gt=0, max_digits=24, decimal_places=8) callback_url: str | None = None class NodeOut(APIModel): id: int user_id: int sn: str node_index: int = 0 display_node_id: int = 0 genesis: bool = False purchase_amount: MoneyText license_months: int licensed_at: datetime license_expires_at: datetime status: NodeStatus last_heartbeat_at: datetime | None callback_url: str | None gpu_model: str | None = None vram_gb: float | None = None cpu_count: int | None = None memory_gb: float | None = None hardware_weight: float = 0.05 gpu_generation: str = "10" recovery_days: int = 540 generation_locked: bool = False recoverable_stake_amount: MoneyText | None = None is_whitelist: bool = False client_ip: str | None = None hardware_fingerprint: str | None = None class NodeView(NodeOut): wallet_address: str online_rate: MoneyText = Decimal("0") today_points: MoneyText = Decimal("0") node_token: str | None = None activated: bool = False class HeartbeatIn(APIModel): sn: SN token: str | None = Field(default=None, max_length=128) gpu_model: str | None = Field(default=None, max_length=128) vram_gb: float | None = Field(default=None, ge=0, le=1024) cpu_count: int | None = Field(default=None, ge=1, le=1024) memory_gb: float | None = Field(default=None, ge=0, le=4096) hardware_fingerprint: str | None = Field(default=None, max_length=64, pattern=r"^[a-fA-F0-9]{64}$") class HeartbeatOut(APIModel): sn: str display_node_id: int = 0 status: NodeStatus last_heartbeat_at: datetime server_time: datetime hardware_weight: float = 0.05 gpu_generation: str = "10" recovery_days: int = 540 recoverable_stake_amount: MoneyText | None = None purchase_amount: MoneyText | None = None estimated_daily_points: MoneyText | None = None class TaskOut(APIModel): task_id: str task_type: str payload: dict status: TaskStatus dispatched_at: datetime deadline_at: datetime class TaskReportIn(APIModel): sn: SN task_id: str = Field(min_length=36, max_length=36) success: bool nonce: str = Field(min_length=8, max_length=128) token: str | None = Field(default=None, max_length=128) class ContributionOut(APIModel): sn: str yield_date: date eligible: bool online_seconds: int eligible_seconds: int online_rate: MoneyText tasks_dispatched: int tasks_completed: int task_response_rate: MoneyText online_weight: MoneyText task_weight: MoneyText performance: MoneyText compute_output: MoneyText points_delta: MoneyText cycle_days: int class YieldOut(APIModel): id: int node_id: int user_id: int yield_date: date compute_output: MoneyText points_delta: MoneyText online_seconds: int eligible_seconds: int online_rate: MoneyText tasks_dispatched: int tasks_completed: int task_response_rate: MoneyText online_weight: MoneyText task_weight: MoneyText performance: MoneyText cycle_days: int status: YieldStatus is_partial: bool created_at: datetime class SettleOut(APIModel): yield_date: date created: int skipped: int total_points: MoneyText is_partial: bool class WithdrawalCreate(APIModel): wallet_address: WALLET amount: Decimal = Field(gt=0, max_digits=24, decimal_places=8) asset_symbol: str = Field(default="POINT", min_length=1, max_length=32) class MeWithdrawalCreate(APIModel): amount: Decimal = Field(gt=0, max_digits=24, decimal_places=8) asset_symbol: str = Field(default="POINT", min_length=1, max_length=32) class MeNodeCreate(APIModel): sn: SN purchase_amount: Decimal = Field(gt=0, max_digits=24, decimal_places=8) callback_url: str | None = None class OrderCreate(APIModel): tx_id: str = Field(min_length=6, max_length=128, pattern=r"^[A-Za-z0-9]+$") class OrderOut(APIModel): id: int user_id: int amount: MoneyText asset_symbol: str status: OrderStatus tx_id: str created_at: datetime reviewed_at: datetime | None = None sn: str | None = None display_node_id: int | None = None class OrderView(OrderOut): wallet_address: str class WithdrawalConfirm(APIModel): tx_hash: str = Field(min_length=6, max_length=128, pattern=r"^[A-Za-z0-9]+$") class WithdrawalReject(APIModel): note: str | None = Field(default=None, max_length=500) class WithdrawalOut(APIModel): id: int user_id: int amount: MoneyText asset_symbol: str tx_hash: str | None status: WithdrawalStatus note: str | None available_after: MoneyText | None = None created_at: datetime updated_at: datetime class WithdrawalView(WithdrawalOut): wallet_address: str class BalanceOut(APIModel): wallet_address: str available_points: MoneyText class SummaryOut(APIModel): users: int nodes: int online: int offline: int authorized_amount: MoneyText settled_points: MoneyText pending_withdrawals: MoneyText class DashboardOut(APIModel): wallet_address: str username: str | None role: UserRole available_points: MoneyText settled_points: MoneyText today_points: MoneyText license_price: MoneyText license_asset: str network_nodes: int = 0 network_online: int = 0 nodes: list[NodeView] orders: list[OrderOut] withdrawals: list[WithdrawalOut] datacenter_applications: list[DatacenterOut] = [] class WhitelistToggle(APIModel): is_whitelist: bool class IPWhitelistCreate(APIModel): ip_address: str = Field(min_length=3, max_length=64) note: str | None = Field(default=None, max_length=128) class IPWhitelistOut(APIModel): id: int ip_address: str note: str | None = None created_at: datetime class DatacenterApply(APIModel): contact: str = Field(min_length=4, max_length=128) node_count: int = Field(ge=1, le=10000) subnet_ip: str = Field(min_length=3, max_length=64) hardware_proof: str = Field(min_length=4, max_length=2000) gpu_generation: str = Field(pattern=r"^(10|30|40)$") class DatacenterOut(APIModel): id: int user_id: int contact: str node_count: int subnet_ip: str hardware_proof: str gpu_generation: str status: DatacenterStatus note: str | None = None created_at: datetime reviewed_at: datetime | None = None class DatacenterView(DatacenterOut): wallet_address: str DashboardOut.model_rebuild()