Files

113 lines
3.6 KiB
Python

"""用户与节点登记。"""
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()