加上登录和角色权限,并用 Vue 控制台区分用户与管理员。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-10 22:43:31 +08:00
parent 78601aae5e
commit 8b55b9cf55
36 changed files with 2947 additions and 1007 deletions
+172 -61
View File
@@ -5,25 +5,34 @@ from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy import func, select
from sqlalchemy import case, func, select
from sqlalchemy.orm import Session, selectinload
from app.clock import utcnow
from app.database import get_db
from app.models import ComputeNode, ComputeTask, NodeStatus, NodeYield, User, WithdrawalLog, WithdrawalStatus, YieldStatus
from app.money import money, to_decimal
from app.deps import AdminUser, CurrentUser
from app.errors import AppError
from app.models import ComputeNode, ComputeTask, User, UserRole, WithdrawalLog, WithdrawalStatus
from app.money import money
from app.schemas import (
AdminUserOut,
BalanceOut,
ContributionOut,
DashboardOut,
HeartbeatIn,
HeartbeatOut,
LoginIn,
MeNodeCreate,
MeWithdrawalCreate,
NodeCreate,
NodeOut,
NodeView,
RegisterIn,
SettleOut,
SummaryOut,
TaskOut,
TaskReportIn,
TokenOut,
UserCreate,
UserOut,
WithdrawalConfirm,
@@ -33,8 +42,18 @@ from app.schemas import (
WithdrawalView,
YieldOut,
)
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.portal import (
authorized_amount,
live_nodes,
node_counts,
pending_withdrawal_amount,
settled_points,
user_rows,
)
from app.services.registry import register_node, register_user
from app.services.tasks import list_pending_tasks, report_task
from app.services.withdrawals import available_balance, confirm_withdrawal, reject_withdrawal, request_withdrawal
@@ -44,35 +63,90 @@ router = APIRouter(prefix="/api/v1")
Db = Annotated[Session, Depends(get_db)]
@router.post("/auth/register", response_model=TokenOut)
def register(body: RegisterIn, db: Db) -> TokenOut:
user = register_account(db, body.wallet_address, body.username, body.password)
return _token(user)
@router.post("/auth/login", response_model=TokenOut)
def login(body: LoginIn, db: Db) -> TokenOut:
user = authenticate(db, body.wallet_address, body.username, body.password)
return _token(user)
@router.get("/me", response_model=UserOut)
def me(user: CurrentUser) -> User:
return user
@router.get("/me/dashboard", response_model=DashboardOut)
def my_dashboard(user: CurrentUser, db: Db) -> DashboardOut:
nodes = live_nodes(db, user.id)
withdrawals = db.scalars(
select(WithdrawalLog).where(WithdrawalLog.user_id == user.id).order_by(WithdrawalLog.id.desc()).limit(20)
).all()
today = money(sum((item["today_points"] for item in nodes), Decimal("0")))
return DashboardOut(
wallet_address=user.wallet_address,
username=user.username,
role=user.role,
available_points=available_balance(db, user.id),
settled_points=settled_points(db, user.id),
today_points=today,
nodes=[NodeView.model_validate(item) for item in nodes],
withdrawals=[WithdrawalOut.model_validate(item) for item in withdrawals],
)
@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)
@router.post("/me/withdrawals", response_model=WithdrawalOut)
def create_my_withdrawal(body: MeWithdrawalCreate, user: CurrentUser, db: Db) -> WithdrawalOut:
row, remaining = request_withdrawal(db, user.wallet_address, body.amount, body.asset_symbol)
return WithdrawalOut.model_validate(row).model_copy(update={"available_after": remaining})
@router.get("/admin/summary", response_model=SummaryOut)
def admin_summary(_: AdminUser, db: Db) -> SummaryOut:
return _summary(db)
@router.get("/admin/users", response_model=list[AdminUserOut])
def admin_users(_: AdminUser, db: Db) -> list[dict]:
return user_rows(db)
@router.get("/admin/nodes", response_model=list[NodeView])
def admin_nodes(_: AdminUser, db: Db) -> list[NodeView]:
return [NodeView.model_validate(item) for item in live_nodes(db)]
@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)]
@router.post("/admin/withdrawals/{withdrawal_id}/confirm", response_model=WithdrawalOut)
def admin_confirm(withdrawal_id: int, body: WithdrawalConfirm, _: AdminUser, db: Db) -> WithdrawalOut:
return confirm_withdrawal(db, withdrawal_id, body.tx_hash)
@router.post("/admin/withdrawals/{withdrawal_id}/reject", response_model=WithdrawalOut)
def admin_reject(withdrawal_id: int, body: WithdrawalReject, _: AdminUser, db: Db) -> WithdrawalOut:
return reject_withdrawal(db, withdrawal_id, body.note)
@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,
)
def summary(_: AdminUser, db: Db) -> SummaryOut:
return _summary(db)
@router.get("/users", response_model=list[UserOut])
def list_users(db: Db) -> list[User]:
def list_users(_: AdminUser, db: Db) -> list[User]:
return list(db.scalars(select(User).order_by(User.id.desc())).all())
@@ -82,21 +156,19 @@ def create_user(body: UserCreate, db: Db) -> UserOut:
@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]
def list_nodes(_: AdminUser, db: Db) -> list[NodeView]:
return [NodeView.model_validate(item) for item in live_nodes(db)]
@router.post("/nodes", response_model=NodeOut)
def create_node(body: NodeCreate, db: Db) -> NodeOut:
def create_node(body: NodeCreate, user: CurrentUser, db: Db) -> NodeOut:
_must_own_wallet(user, body.wallet_address)
return register_node(db, body.wallet_address, body.sn, body.purchase_amount, body.callback_url)
@router.get("/nodes/{sn}", response_model=NodeOut)
def get_node(sn: str, db: Db) -> NodeOut:
return get_node_by_sn(db, sn)
def get_node(sn: str, user: CurrentUser, db: Db) -> NodeOut:
return _owned_node(db, user, sn)
@router.post("/node/heartbeat", response_model=HeartbeatOut)
@@ -121,14 +193,14 @@ def report(body: TaskReportIn, db: Db) -> TaskOut:
@router.get("/nodes/{sn}/contribution", response_model=ContributionOut)
def contribution(sn: str, yield_date: date, db: Db) -> ContributionOut:
node = get_node_by_sn(db, sn)
def contribution(sn: str, yield_date: date, user: CurrentUser, db: Db) -> ContributionOut:
node = _owned_node(db, user, sn)
computed = compute_node_yield(db, node, yield_date)
return ContributionOut(sn=node.sn, **computed.__dict__)
@router.post("/yield/settle", response_model=SettleOut)
def settle(db: Db, yield_date: date | None = None) -> SettleOut:
def settle(_: AdminUser, db: Db, yield_date: date | None = None) -> SettleOut:
target = yield_date or (utcnow().date() - timedelta(days=1))
summary = settle_yield_date(db, target)
return SettleOut(
@@ -141,52 +213,91 @@ def settle(db: Db, yield_date: date | None = None) -> SettleOut:
@router.get("/nodes/{sn}/yields", response_model=list[YieldOut])
def yields(sn: str, db: Db) -> list[YieldOut]:
def yields(sn: str, user: CurrentUser, db: Db) -> list[YieldOut]:
_owned_node(db, user, sn)
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
]
def list_withdrawals(_: AdminUser, db: Db) -> list[WithdrawalView]:
return [_withdrawal_view(row) for row in _withdrawal_rows(db)]
@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)
return BalanceOut(wallet_address=user.wallet_address, available_points=available_balance(db, user.id))
def balance(wallet_address: str, user: CurrentUser, db: Db) -> BalanceOut:
_must_own_wallet(user, wallet_address)
owner = get_user_by_wallet(db, wallet_address)
return BalanceOut(wallet_address=owner.wallet_address, available_points=available_balance(db, owner.id))
@router.post("/withdrawals", response_model=WithdrawalOut)
def create_withdrawal(body: WithdrawalCreate, db: Db) -> WithdrawalOut:
def create_withdrawal(body: WithdrawalCreate, user: CurrentUser, db: Db) -> WithdrawalOut:
_must_own_wallet(user, body.wallet_address)
row, remaining = request_withdrawal(db, body.wallet_address, body.amount, body.asset_symbol)
return WithdrawalOut.model_validate(row).model_copy(update={"available_after": remaining})
@router.post("/withdrawals/{withdrawal_id}/confirm", response_model=WithdrawalOut)
def confirm(withdrawal_id: int, body: WithdrawalConfirm, db: Db) -> WithdrawalOut:
def confirm(withdrawal_id: int, body: WithdrawalConfirm, _: AdminUser, db: Db) -> WithdrawalOut:
return confirm_withdrawal(db, withdrawal_id, body.tx_hash)
@router.post("/withdrawals/{withdrawal_id}/reject", response_model=WithdrawalOut)
def reject(withdrawal_id: int, body: WithdrawalReject, db: Db) -> WithdrawalOut:
def reject(withdrawal_id: int, body: WithdrawalReject, _: AdminUser, 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 _summary(db: Session) -> SummaryOut:
users = db.scalar(select(func.count()).select_from(User)) or 0
nodes, online = node_counts(db)
return SummaryOut(
users=users,
nodes=nodes,
online=online,
offline=nodes - online,
authorized_amount=authorized_amount(db),
settled_points=settled_points(db),
pending_withdrawals=pending_withdrawal_amount(db),
)
def _sum(values: list[object]) -> Decimal:
return money(sum((to_decimal(item) for item in values), Decimal("0")))
def _token(user: User) -> TokenOut:
return TokenOut(
access_token=create_access_token(user),
role=user.role,
wallet_address=user.wallet_address,
username=user.username,
)
def _must_own_wallet(user: User, wallet_address: str) -> None:
if user.role != UserRole.ADMIN and user.wallet_address != wallet_address.strip():
raise AppError("只能操作自己的钱包", 403)
def _owned_node(db: Session, user: User, sn: str) -> ComputeNode:
node = get_node_by_sn(db, sn)
if user.role != UserRole.ADMIN and node.user_id != user.id:
raise AppError("无权查看该节点", 403)
return node
def _withdrawal_rows(db: Session) -> list[WithdrawalLog]:
pending_first = case((WithdrawalLog.status == WithdrawalStatus.PENDING, 0), else_=1)
return list(
db.scalars(
select(WithdrawalLog)
.options(selectinload(WithdrawalLog.user))
.order_by(pending_first, WithdrawalLog.id.desc())
).all()
)
def _withdrawal_view(row: WithdrawalLog) -> WithdrawalView:
return WithdrawalView(
**WithdrawalOut.model_validate(row).model_dump(),
wallet_address=row.user.wallet_address,
)
def _task_out(task: ComputeTask) -> TaskOut: