Files
HyperNode-AI/app/services/orders.py
T

272 lines
8.4 KiB
Python

"""节点授权订单。付款凭证先入库,管理员通过后才开通节点。"""
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