订单需管理员审核后才开通节点,并更新登录页文案。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-10 23:04:24 +08:00
parent 8b55b9cf55
commit f329f647c7
22 changed files with 583 additions and 81 deletions
+12 -1
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import hmac
from datetime import datetime, timedelta
from sqlalchemy import select
@@ -14,10 +15,20 @@ from app.models import ComputeNode, NodeStatus, UptimeInterval
from app.services.common import get_node_by_sn
def record_heartbeat(session: Session, sn: str, now: datetime | None = None) -> ComputeNode:
def assert_node_token(node: ComputeNode, token: str | None) -> None:
"""旧节点没有 Token 时仍按序列号接入。审核开通的节点必须带上专属 Token。"""
if not node.node_token:
return
supplied = (token or "").strip()
if not supplied or not hmac.compare_digest(node.node_token, supplied):
raise AppError("节点 Token 不正确", 403)
def record_heartbeat(session: Session, sn: str, token: str | None = None, now: datetime | None = None) -> ComputeNode:
"""刷新最后活跃时间。离线节点收到心跳后重新打开一段在线区间。"""
moment = now or utcnow()
node = get_node_by_sn(session, sn.strip())
assert_node_token(node, token)
if node.license_expires_at <= moment:
raise AppError("节点授权已到期", 403)
+117
View File
@@ -0,0 +1,117 @@
"""节点授权订单。付款凭证先入库,管理员通过后才开通节点。"""
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())
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 f"""# HyperNode 挂机客户端。这个文件里有节点 Token,不要发给别人。
import json
import time
import urllib.error
import urllib.request
CONFIG = json.loads({config!r})
def beat():
body = json.dumps({{"sn": CONFIG["sn"], "token": CONFIG["token"]}}).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 _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
+2
View File
@@ -63,6 +63,8 @@ def live_nodes(session: Session, user_id: int | None = None) -> list[dict]:
"status": node.status,
"last_heartbeat_at": node.last_heartbeat_at,
"callback_url": node.callback_url,
"node_token": node.node_token if user_id is not None else None,
"activated": node.node_token is not None,
"online_rate": computed.online_rate,
"today_points": computed.points_delta if computed.eligible else Decimal("0"),
}
+2
View File
@@ -51,6 +51,7 @@ def register_node(
sn: str,
purchase_amount: Decimal,
callback_url: str | None = None,
node_token: str | None = None,
) -> ComputeNode:
user = get_user_by_wallet(session, wallet_address.strip())
serial = sn.strip()
@@ -67,6 +68,7 @@ def register_node(
license_expires_at=add_months(started, settings.license_months),
status=NodeStatus.OFFLINE,
callback_url=callback,
node_token=node_token,
created_at=started,
updated_at=started,
)