"""用户与节点登记。""" from __future__ import annotations from decimal import Decimal from urllib.parse import urlparse from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.clock import add_months, utcnow from app.config import settings from app.errors import AppError from app.models import ComputeNode, NodeStatus, User, UserRole from app.money import money from app.services.common import get_user_by_wallet from app.services.ip_guard import assert_ip_available from app.services.hardware import ( DEFAULT_WEIGHT, GENERATION_10, GENESIS_RECOVERY_DAYS, RECOVERY_10_DAYS, is_genesis, ) def register_user( session: Session, wallet_address: str, username: str | None = None, password_hash: str | None = None, role: UserRole = UserRole.USER, ) -> User: wallet = wallet_address.strip() if session.scalar(select(User).where(User.wallet_address == wallet)) is not None: raise AppError("钱包地址已注册", 409) if username and session.scalar(select(User).where(User.username == username)) is not None: raise AppError("用户名已占用", 409) user = User( wallet_address=wallet, username=username, password_hash=password_hash, role=role, created_at=utcnow(), ) session.add(user) try: session.flush() except IntegrityError as exc: session.rollback() raise AppError("钱包地址或用户名已注册", 409) from exc return user def register_node( session: Session, wallet_address: str, sn: str, purchase_amount: Decimal, callback_url: str | None = None, node_token: str | None = None, client_ip: str | None = None, ) -> ComputeNode: user = get_user_by_wallet(session, wallet_address.strip()) serial = sn.strip() if session.scalar(select(ComputeNode).where(ComputeNode.sn == serial)) is not None: raise AppError("节点序列号已存在", 409) bound_ip = (client_ip or "").strip() or None if bound_ip: assert_ip_available(session, bound_ip) callback = _normalize_callback(callback_url) started = utcnow() node_index = int(session.scalar(select(func.max(ComputeNode.node_index))) or 0) + 1 recovery_days = GENESIS_RECOVERY_DAYS if is_genesis(node_index) else RECOVERY_10_DAYS node = ComputeNode( user_id=user.id, sn=serial, node_index=node_index, purchase_amount=money(purchase_amount), license_months=settings.license_months, licensed_at=started, license_expires_at=add_months(started, settings.license_months), status=NodeStatus.OFFLINE, callback_url=callback, node_token=node_token, hardware_weight=float(DEFAULT_WEIGHT), gpu_generation=GENERATION_10, recovery_days=recovery_days, generation_locked=False, recoverable_stake_amount=money(purchase_amount), is_whitelist=False, client_ip=bound_ip, created_at=started, updated_at=started, ) session.add(node) try: session.flush() except IntegrityError as exc: session.rollback() raise AppError("节点序列号已存在", 409) from exc return node def _normalize_callback(callback_url: str | None) -> str | None: if callback_url is None or callback_url.strip() == "": return None parsed = urlparse(callback_url.strip()) if parsed.scheme not in {"http", "https"} or not parsed.netloc: raise AppError("callback_url 必须是 http 或 https 地址") return callback_url.strip()