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

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
+68 -5
View File
@@ -4,15 +4,17 @@ from datetime import date, timedelta
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Request
from fastapi.responses import Response
from sqlalchemy import case, func, select
from sqlalchemy.orm import Session, selectinload
from app.clock import utcnow
from app.config import settings
from app.database import get_db
from app.deps import AdminUser, CurrentUser
from app.errors import AppError
from app.models import ComputeNode, ComputeTask, User, UserRole, WithdrawalLog, WithdrawalStatus
from app.models import ComputeNode, ComputeTask, LicenseOrder, User, UserRole, WithdrawalLog, WithdrawalStatus
from app.money import money
from app.schemas import (
AdminUserOut,
@@ -23,6 +25,9 @@ from app.schemas import (
HeartbeatOut,
LoginIn,
MeNodeCreate,
OrderCreate,
OrderOut,
OrderView,
MeWithdrawalCreate,
NodeCreate,
NodeOut,
@@ -45,7 +50,8 @@ from app.schemas import (
from app.security import create_access_token
from app.services.auth import authenticate, register_account
from app.services.common import get_node_by_sn, get_user_by_wallet
from app.services.heartbeat import record_heartbeat
from app.services.heartbeat import assert_node_token, record_heartbeat
from app.services.orders import approve_order, list_orders, place_order, reject_order, render_client
from app.services.portal import (
authorized_amount,
live_nodes,
@@ -95,10 +101,32 @@ def my_dashboard(user: CurrentUser, db: Db) -> DashboardOut:
settled_points=settled_points(db, user.id),
today_points=today,
nodes=[NodeView.model_validate(item) for item in nodes],
orders=[_order_out(item) for item in list_orders(db, user.id)],
license_price=money(settings.license_price),
license_asset=settings.license_asset,
withdrawals=[WithdrawalOut.model_validate(item) for item in withdrawals],
)
@router.post("/me/orders", response_model=OrderOut)
def create_my_order(body: OrderCreate, user: CurrentUser, db: Db) -> OrderOut:
return _order_out(place_order(db, user.id, body.tx_id))
@router.get("/me/nodes/{sn}/client")
def download_client(sn: str, request: Request, user: CurrentUser, db: Db) -> Response:
node = _owned_node(db, user, sn)
if not node.node_token:
raise AppError("节点尚未开通,不能下载客户端", 403)
script = render_client(node.sn, node.node_token, str(request.base_url))
filename = f"hypernode-{node.sn}.py"
return Response(
content=script,
media_type="text/x-python; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.post("/me/nodes", response_model=NodeOut)
def create_my_node(body: MeNodeCreate, user: CurrentUser, db: Db) -> NodeOut:
return register_node(db, user.wallet_address, body.sn, body.purchase_amount, body.callback_url)
@@ -125,6 +153,21 @@ def admin_nodes(_: AdminUser, db: Db) -> list[NodeView]:
return [NodeView.model_validate(item) for item in live_nodes(db)]
@router.get("/admin/orders", response_model=list[OrderView])
def admin_orders(_: AdminUser, db: Db) -> list[OrderView]:
return [_order_view(row) for row in list_orders(db)]
@router.post("/admin/orders/{order_id}/approve", response_model=OrderView)
def admin_approve_order(order_id: int, _: AdminUser, db: Db) -> OrderView:
return _order_view(approve_order(db, order_id))
@router.post("/admin/orders/{order_id}/reject", response_model=OrderView)
def admin_reject_order(order_id: int, body: WithdrawalReject, _: AdminUser, db: Db) -> OrderView:
return _order_view(reject_order(db, order_id, body.note))
@router.get("/admin/withdrawals", response_model=list[WithdrawalView])
def admin_withdrawals(_: AdminUser, db: Db) -> list[WithdrawalView]:
return [_withdrawal_view(row) for row in _withdrawal_rows(db)]
@@ -173,7 +216,7 @@ def get_node(sn: str, user: CurrentUser, db: Db) -> NodeOut:
@router.post("/node/heartbeat", response_model=HeartbeatOut)
def heartbeat(body: HeartbeatIn, db: Db) -> HeartbeatOut:
node = record_heartbeat(db, body.sn)
node = record_heartbeat(db, body.sn, body.token)
return HeartbeatOut(
sn=node.sn,
status=node.status,
@@ -183,12 +226,14 @@ def heartbeat(body: HeartbeatIn, db: Db) -> HeartbeatOut:
@router.get("/node/{sn}/tasks", response_model=list[TaskOut])
def pending_tasks(sn: str, db: Db) -> list[TaskOut]:
def pending_tasks(sn: str, db: Db, token: str | None = None) -> list[TaskOut]:
assert_node_token(get_node_by_sn(db, sn), token)
return [_task_out(task) for task in list_pending_tasks(db, sn)]
@router.post("/node/task/report", response_model=TaskOut)
def report(body: TaskReportIn, db: Db) -> TaskOut:
assert_node_token(get_node_by_sn(db, body.sn), body.token)
return _task_out(report_task(db, body.sn, body.task_id, body.success, body.nonce))
@@ -293,6 +338,24 @@ def _withdrawal_rows(db: Session) -> list[WithdrawalLog]:
)
def _order_out(order: LicenseOrder) -> OrderOut:
return OrderOut(
id=order.id,
user_id=order.user_id,
amount=order.amount,
asset_symbol=order.asset_symbol,
status=order.status,
tx_id=order.tx_id,
created_at=order.created_at,
reviewed_at=order.reviewed_at,
sn=order.node.sn if order.node is not None else None,
)
def _order_view(order: LicenseOrder) -> OrderView:
return OrderView(**_order_out(order).model_dump(), wallet_address=order.user.wallet_address)
def _withdrawal_view(row: WithdrawalLog) -> WithdrawalView:
return WithdrawalView(
**WithdrawalOut.model_validate(row).model_dump(),