订单需管理员审核后才开通节点,并更新登录页文案。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user