提供浏览器控制台,并补齐列表接口供页面读取。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-10 22:20:45 +08:00
parent 2a847263f2
commit 78601aae5e
8 changed files with 1020 additions and 4 deletions
+70 -2
View File
@@ -1,14 +1,17 @@
"""对外 HTTP 接口。业务规则在 services 中,这里只做参数转发。"""
from datetime import date, timedelta
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from sqlalchemy import func, select
from sqlalchemy.orm import Session, selectinload
from app.clock import utcnow
from app.database import get_db
from app.models import ComputeTask
from app.models import ComputeNode, ComputeTask, NodeStatus, NodeYield, User, WithdrawalLog, WithdrawalStatus, YieldStatus
from app.money import money, to_decimal
from app.schemas import (
BalanceOut,
ContributionOut,
@@ -16,7 +19,9 @@ from app.schemas import (
HeartbeatOut,
NodeCreate,
NodeOut,
NodeView,
SettleOut,
SummaryOut,
TaskOut,
TaskReportIn,
UserCreate,
@@ -25,6 +30,7 @@ from app.schemas import (
WithdrawalCreate,
WithdrawalOut,
WithdrawalReject,
WithdrawalView,
YieldOut,
)
from app.services.common import get_node_by_sn, get_user_by_wallet
@@ -38,11 +44,51 @@ router = APIRouter(prefix="/api/v1")
Db = Annotated[Session, Depends(get_db)]
@router.get("/summary", response_model=SummaryOut)
def summary(db: Db) -> SummaryOut:
users = db.scalar(select(func.count()).select_from(User)) or 0
nodes = db.scalar(select(func.count()).select_from(ComputeNode)) or 0
online = db.scalar(
select(func.count()).select_from(ComputeNode).where(ComputeNode.status == NodeStatus.ONLINE)
) or 0
authorized = _sum(db.scalars(select(ComputeNode.purchase_amount)).all())
settled = _sum(
db.scalars(select(NodeYield.points_delta).where(NodeYield.status == YieldStatus.SETTLED)).all()
)
pending = _sum(
db.scalars(
select(WithdrawalLog.amount).where(WithdrawalLog.status == WithdrawalStatus.PENDING)
).all()
)
return SummaryOut(
users=users,
nodes=nodes,
online=online,
offline=nodes - online,
authorized_amount=authorized,
settled_points=settled,
pending_withdrawals=pending,
)
@router.get("/users", response_model=list[UserOut])
def list_users(db: Db) -> list[User]:
return list(db.scalars(select(User).order_by(User.id.desc())).all())
@router.post("/users", response_model=UserOut)
def create_user(body: UserCreate, db: Db) -> UserOut:
return register_user(db, body.wallet_address)
@router.get("/nodes", response_model=list[NodeView])
def list_nodes(db: Db) -> list[NodeView]:
rows = db.scalars(
select(ComputeNode).options(selectinload(ComputeNode.user)).order_by(ComputeNode.id.desc())
).all()
return [_node_view(row) for row in rows]
@router.post("/nodes", response_model=NodeOut)
def create_node(body: NodeCreate, db: Db) -> NodeOut:
return register_node(db, body.wallet_address, body.sn, body.purchase_amount, body.callback_url)
@@ -99,6 +145,20 @@ def yields(sn: str, db: Db) -> list[YieldOut]:
return list_node_yields(db, sn)
@router.get("/withdrawals", response_model=list[WithdrawalView])
def list_withdrawals(db: Db) -> list[WithdrawalView]:
rows = db.scalars(
select(WithdrawalLog).options(selectinload(WithdrawalLog.user)).order_by(WithdrawalLog.id.desc())
).all()
return [
WithdrawalView(
**WithdrawalOut.model_validate(row).model_dump(),
wallet_address=row.user.wallet_address,
)
for row in rows
]
@router.get("/users/{wallet_address}/balance", response_model=BalanceOut)
def balance(wallet_address: str, db: Db) -> BalanceOut:
user = get_user_by_wallet(db, wallet_address)
@@ -121,6 +181,14 @@ def reject(withdrawal_id: int, body: WithdrawalReject, db: Db) -> WithdrawalOut:
return reject_withdrawal(db, withdrawal_id, body.note)
def _node_view(node: ComputeNode) -> NodeView:
return NodeView(**NodeOut.model_validate(node).model_dump(), wallet_address=node.user.wallet_address)
def _sum(values: list[object]) -> Decimal:
return money(sum((to_decimal(item) for item in values), Decimal("0")))
def _task_out(task: ComputeTask) -> TaskOut:
return TaskOut(
task_id=task.id,
+12 -2
View File
@@ -4,9 +4,11 @@ from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
import app.models # noqa: F401 注册全部表
from app.api import router
@@ -27,8 +29,16 @@ async def lifespan(_: FastAPI):
stop_scheduler()
app = FastAPI(title="DePIN 算力节点与任务调度", version="1.0.0", lifespan=lifespan)
WEB_DIR = Path(__file__).resolve().parent.parent / "web"
app = FastAPI(title="HyperNode-AI", version="1.0.0", lifespan=lifespan)
app.include_router(router)
app.mount("/static", StaticFiles(directory=WEB_DIR), name="static")
@app.get("/")
def console() -> FileResponse:
return FileResponse(WEB_DIR / "index.html")
@app.exception_handler(AppError)
+18
View File
@@ -49,6 +49,10 @@ class NodeOut(APIModel):
callback_url: str | None
class NodeView(NodeOut):
wallet_address: str
class HeartbeatIn(APIModel):
sn: SN
@@ -151,6 +155,20 @@ class WithdrawalOut(APIModel):
updated_at: datetime
class WithdrawalView(WithdrawalOut):
wallet_address: str
class BalanceOut(APIModel):
wallet_address: str
available_points: MoneyText
class SummaryOut(APIModel):
users: int
nodes: int
online: int
offline: int
authorized_amount: MoneyText
settled_points: MoneyText
pending_withdrawals: MoneyText