"""节点授权订单。付款凭证先入库,管理员通过后才开通节点。""" from __future__ import annotations import json import secrets from sqlalchemy import case, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, selectinload from app.clock import utcnow from app.config import settings from app.errors import AppError from app.models import LicenseOrder, OrderStatus from app.money import money from app.services.registry import register_node def place_order(session: Session, user_id: int, tx_id: str) -> LicenseOrder: digest = tx_id.strip() if session.scalar(select(LicenseOrder.id).where(LicenseOrder.tx_id == digest)) is not None: raise AppError("这笔交易已经提交过", 409) order = LicenseOrder( user_id=user_id, amount=money(settings.license_price), asset_symbol=settings.license_asset.strip().upper() or "USDT", status=OrderStatus.PENDING, tx_id=digest, created_at=utcnow(), ) session.add(order) try: session.flush() except IntegrityError as exc: session.rollback() raise AppError("这笔交易已经提交过", 409) from exc return order def approve_order(session: Session, order_id: int) -> LicenseOrder: order = _pending_order(session, order_id) token = secrets.token_urlsafe(24) node = register_node( session, order.user.wallet_address, f"HN{order.id:06d}", order.amount, node_token=token, ) order.status = OrderStatus.APPROVED order.node = node order.reviewed_at = utcnow() session.flush() return order def reject_order(session: Session, order_id: int, note: str | None = None) -> LicenseOrder: order = _pending_order(session, order_id) order.status = OrderStatus.REJECTED order.note = note order.reviewed_at = utcnow() session.flush() return order def list_orders(session: Session, user_id: int | None = None) -> list[LicenseOrder]: pending_first = case((LicenseOrder.status == OrderStatus.PENDING, 0), else_=1) query = select(LicenseOrder).options(selectinload(LicenseOrder.user), selectinload(LicenseOrder.node)) if user_id is not None: query = query.where(LicenseOrder.user_id == user_id) return list(session.scalars(query.order_by(pending_first, LicenseOrder.id.desc())).all()) _CLIENT = """# HyperNode 挂机客户端。这个文件里有节点 Token,不要发给别人。 import ctypes import hashlib import json import os import subprocess import time import urllib.error import urllib.request import uuid CONFIG = json.loads(__CONFIG__) def hardware(): gpu_model, vram_gb, gpu_uuid = _gpu() return { "sn": CONFIG["sn"], "token": CONFIG["token"], "gpu_model": gpu_model, "vram_gb": vram_gb, "cpu_count": os.cpu_count(), "memory_gb": _memory_gb(), "hardware_fingerprint": _fingerprint(gpu_uuid), } def _fingerprint(gpu_uuid): board = _board_serial() or "" mac = f"{uuid.getnode():012x}" material = "|".join([gpu_uuid or "", board, mac]) if not material.strip("|"): return None return hashlib.sha256(material.encode()).hexdigest() def _board_serial(): if os.name == "nt": try: output = subprocess.check_output( ["wmic", "baseboard", "get", "serialnumber"], stderr=subprocess.DEVNULL, timeout=8, text=True, ) except (OSError, subprocess.SubprocessError): return None lines = [line.strip() for line in output.splitlines() if line.strip() and "SerialNumber" not in line] return lines[0] if lines else None for path in ("/sys/class/dmi/id/board_serial", "/sys/class/dmi/id/product_uuid"): try: with open(path, encoding="utf-8") as handle: value = handle.read().strip() except OSError: continue if value and value.lower() not in {"none", "to be filled by o.e.m."}: return value return None def _gpu(): probed = _pynvml() if probed[0]: return probed return _nvidia_smi() def _pynvml(): try: import pynvml except ImportError: return None, None, None try: pynvml.nvmlInit() best_name = None best_bytes = -1 best_uuid = None for index in range(pynvml.nvmlDeviceGetCount()): handle = pynvml.nvmlDeviceGetHandleByIndex(index) name = pynvml.nvmlDeviceGetName(handle) if isinstance(name, bytes): name = name.decode() total = int(pynvml.nvmlDeviceGetMemoryInfo(handle).total) if total > best_bytes: uid = pynvml.nvmlDeviceGetUUID(handle) if isinstance(uid, bytes): uid = uid.decode() best_name = str(name) best_bytes = total best_uuid = str(uid) pynvml.nvmlShutdown() except Exception: return None, None, None if not best_name or best_bytes < 0: return None, None, None return best_name, round(best_bytes / (1024 ** 3), 2), best_uuid def _nvidia_smi(): try: output = subprocess.check_output( ["nvidia-smi", "--query-gpu=name,memory.total,uuid", "--format=csv,noheader,nounits"], stderr=subprocess.DEVNULL, timeout=8, text=True, ) except (OSError, subprocess.SubprocessError): return None, None, None best_name = None best_mib = -1.0 best_uuid = None for line in output.splitlines(): parts = [part.strip() for part in line.split(",", 2)] if len(parts) < 2: continue name, memory = parts[0], parts[1] gpu_uuid = parts[2] if len(parts) > 2 else None try: mib = float(memory) except ValueError: continue if mib > best_mib: best_name = name or None best_mib = mib best_uuid = gpu_uuid or None if best_name is None: return None, None, None return best_name, round(best_mib / 1024, 2), best_uuid def _memory_gb(): if os.name == "nt": class MemoryStatus(ctypes.Structure): _fields_ = [ ("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong), ("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong), ("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong), ("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong), ("ullAvailExtendedVirtual", ctypes.c_ulonglong), ] status = MemoryStatus() status.dwLength = ctypes.sizeof(MemoryStatus) if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)): return round(status.ullTotalPhys / (1024 ** 3), 2) return None try: with open("/proc/meminfo", encoding="utf-8") as handle: for line in handle: if line.startswith("MemTotal:"): return round(int(line.split()[1]) / 1024 / 1024, 2) except OSError: return None return None def beat(): body = json.dumps(hardware()).encode() request = urllib.request.Request( CONFIG["base"] + "/api/v1/node/heartbeat", data=body, headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(request, timeout=20) as response: print(response.read().decode()) if __name__ == "__main__": while True: try: beat() except (urllib.error.URLError, TimeoutError, OSError) as exc: print(exc) time.sleep(60) """ def render_client(sn: str, token: str, base_url: str) -> str: config = json.dumps({"sn": sn, "token": token, "base": base_url.rstrip("/")}, ensure_ascii=True) return _CLIENT.replace("__CONFIG__", repr(config)) def _pending_order(session: Session, order_id: int) -> LicenseOrder: order = session.scalar( select(LicenseOrder) .options(selectinload(LicenseOrder.user), selectinload(LicenseOrder.node)) .where(LicenseOrder.id == order_id) ) if order is None: raise AppError("订单不存在", 404) if order.status != OrderStatus.PENDING: raise AppError("只有待审核的订单可以处理", 409) return order