按显卡世代和创世编号结算回本,机房批量接入需审核网段并用硬件指纹防克隆。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-11 00:30:46 +08:00
parent 7d29be8b17
commit 0740016ad8
22 changed files with 1554 additions and 96 deletions
+132 -11
View File
@@ -14,7 +14,17 @@ 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, LicenseOrder, User, UserRole, WithdrawalLog, WithdrawalStatus
from app.models import (
ComputeNode,
ComputeTask,
DatacenterApplication,
IPWhitelist,
LicenseOrder,
User,
UserRole,
WithdrawalLog,
WithdrawalStatus,
)
from app.money import money
from app.schemas import (
AdminUserOut,
@@ -32,6 +42,9 @@ from app.schemas import (
NodeCreate,
NodeOut,
NodeView,
IPWhitelistCreate,
IPWhitelistOut,
WhitelistToggle,
RegisterIn,
SettleOut,
SummaryOut,
@@ -43,6 +56,9 @@ from app.schemas import (
WithdrawalConfirm,
WithdrawalCreate,
WithdrawalOut,
DatacenterApply,
DatacenterOut,
DatacenterView,
WithdrawalReject,
WithdrawalView,
YieldOut,
@@ -50,7 +66,11 @@ 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.datacenter import approve_application, list_applications, reject_application, submit_application
from app.services.hardware import HardwareReport
from app.services.hardware import display_node_id, displayed_population
from app.services.heartbeat import assert_node_token, record_heartbeat
from app.services.ip_guard import add_whitelist_ip, client_ip, list_whitelist, remove_whitelist_ip
from app.services.orders import approve_order, list_orders, place_order, reject_order, render_client
from app.services.portal import (
authorized_amount,
@@ -63,7 +83,7 @@ from app.services.portal import (
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
from app.services.yield_engine import compute_node_yield, list_node_yields, settle_yield_date
from app.services.yield_engine import compute_node_yield, list_node_yields, network_yields, settle_yield_date
router = APIRouter(prefix="/api/v1")
Db = Annotated[Session, Depends(get_db)]
@@ -81,6 +101,12 @@ def login(body: LoginIn, db: Db) -> TokenOut:
return _token(user)
@router.get("/network")
def network_population(db: Db) -> dict[str, int]:
nodes, online = node_counts(db)
return {"nodes": displayed_population(nodes), "online": displayed_population(online)}
@router.get("/me", response_model=UserOut)
def me(user: CurrentUser) -> User:
return user
@@ -93,6 +119,7 @@ def my_dashboard(user: CurrentUser, db: Db) -> DashboardOut:
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")))
network_total, network_online = node_counts(db)
return DashboardOut(
wallet_address=user.wallet_address,
username=user.username,
@@ -104,7 +131,10 @@ def my_dashboard(user: CurrentUser, db: Db) -> DashboardOut:
orders=[_order_out(item) for item in list_orders(db, user.id)],
license_price=money(settings.license_price),
license_asset=settings.license_asset,
network_nodes=displayed_population(network_total),
network_online=displayed_population(network_online),
withdrawals=[WithdrawalOut.model_validate(item) for item in withdrawals],
datacenter_applications=[_datacenter_out(item) for item in list_applications(db, user.id)],
)
@@ -128,8 +158,25 @@ def download_client(sn: str, request: Request, user: CurrentUser, db: Db) -> Res
@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)
def create_my_node(body: MeNodeCreate, request: Request, user: CurrentUser, db: Db) -> NodeOut:
return register_node(
db, user.wallet_address, body.sn, body.purchase_amount, body.callback_url, client_ip=client_ip(request)
)
@router.post("/me/datacenter-applications", response_model=DatacenterOut)
def create_datacenter_application(body: DatacenterApply, user: CurrentUser, db: Db) -> DatacenterOut:
return _datacenter_out(
submit_application(
db,
user.id,
body.contact,
body.node_count,
body.subnet_ip,
body.hardware_proof,
body.gpu_generation,
)
)
@router.post("/me/withdrawals", response_model=WithdrawalOut)
@@ -153,6 +200,46 @@ def admin_nodes(_: AdminUser, db: Db) -> list[NodeView]:
return [NodeView.model_validate(item) for item in live_nodes(db)]
@router.post("/admin/nodes/{sn}/whitelist", response_model=NodeView)
def admin_toggle_node_whitelist(sn: str, body: WhitelistToggle, _: AdminUser, db: Db) -> NodeView:
node = get_node_by_sn(db, sn)
node.is_whitelist = body.is_whitelist
node.updated_at = utcnow()
db.flush()
snapshot = next(item for item in live_nodes(db) if item["sn"] == node.sn)
return NodeView.model_validate(snapshot)
@router.get("/admin/ip-whitelist", response_model=list[IPWhitelistOut])
def admin_ip_whitelist(_: AdminUser, db: Db) -> list[IPWhitelist]:
return list_whitelist(db)
@router.post("/admin/ip-whitelist", response_model=IPWhitelistOut)
def admin_add_ip_whitelist(body: IPWhitelistCreate, _: AdminUser, db: Db) -> IPWhitelist:
return add_whitelist_ip(db, body.ip_address, body.note)
@router.delete("/admin/ip-whitelist/{entry_id}", response_model=IPWhitelistOut)
def admin_remove_ip_whitelist(entry_id: int, _: AdminUser, db: Db) -> IPWhitelist:
return remove_whitelist_ip(db, entry_id)
@router.get("/admin/datacenter-applications", response_model=list[DatacenterView])
def admin_datacenter_applications(_: AdminUser, db: Db) -> list[DatacenterView]:
return [_datacenter_view(row) for row in list_applications(db)]
@router.post("/admin/datacenter-applications/{application_id}/approve", response_model=DatacenterView)
def admin_approve_datacenter(application_id: int, _: AdminUser, db: Db) -> DatacenterView:
return _datacenter_view(approve_application(db, application_id))
@router.post("/admin/datacenter-applications/{application_id}/reject", response_model=DatacenterView)
def admin_reject_datacenter(application_id: int, body: WithdrawalReject, _: AdminUser, db: Db) -> DatacenterView:
return _datacenter_view(reject_application(db, application_id, body.note))
@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)]
@@ -204,9 +291,11 @@ def list_nodes(_: AdminUser, db: Db) -> list[NodeView]:
@router.post("/nodes", response_model=NodeOut)
def create_node(body: NodeCreate, user: CurrentUser, db: Db) -> NodeOut:
def create_node(body: NodeCreate, request: Request, 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)
return register_node(
db, body.wallet_address, body.sn, body.purchase_amount, body.callback_url, client_ip=client_ip(request)
)
@router.get("/nodes/{sn}", response_model=NodeOut)
@@ -215,13 +304,28 @@ 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, body.token)
def heartbeat(body: HeartbeatIn, request: Request, db: Db) -> HeartbeatOut:
hardware = None
if {"gpu_model", "vram_gb", "cpu_count", "memory_gb"} & body.model_fields_set:
hardware = HardwareReport(body.gpu_model, body.vram_gb, body.cpu_count, body.memory_gb)
node = record_heartbeat(
db,
body.sn,
body.token,
hardware=hardware,
client_ip=client_ip(request),
hardware_fingerprint=body.hardware_fingerprint,
)
return HeartbeatOut(
sn=node.sn,
display_node_id=node.display_node_id,
status=node.status,
last_heartbeat_at=node.last_heartbeat_at,
server_time=node.last_heartbeat_at,
hardware_weight=node.hardware_weight,
gpu_generation=node.gpu_generation,
recovery_days=node.recovery_days,
recoverable_stake_amount=node.recoverable_stake_amount,
)
@@ -240,7 +344,12 @@ def report(body: TaskReportIn, db: Db) -> TaskOut:
@router.get("/nodes/{sn}/contribution", response_model=ContributionOut)
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)
computed = next(
(item for current, item in network_yields(db, yield_date) if current.id == node.id),
None,
)
if computed is None:
computed = compute_node_yield(db, node, yield_date)
return ContributionOut(sn=node.sn, **computed.__dict__)
@@ -297,8 +406,8 @@ def _summary(db: Session) -> SummaryOut:
nodes, online = node_counts(db)
return SummaryOut(
users=users,
nodes=nodes,
online=online,
nodes=displayed_population(nodes),
online=displayed_population(online),
offline=nodes - online,
authorized_amount=authorized_amount(db),
settled_points=settled_points(db),
@@ -338,6 +447,17 @@ def _withdrawal_rows(db: Session) -> list[WithdrawalLog]:
)
def _datacenter_out(row: DatacenterApplication) -> DatacenterOut:
return DatacenterOut.model_validate(row)
def _datacenter_view(row: DatacenterApplication) -> DatacenterView:
return DatacenterView(
**_datacenter_out(row).model_dump(),
wallet_address=row.user.wallet_address,
)
def _order_out(order: LicenseOrder) -> OrderOut:
return OrderOut(
id=order.id,
@@ -349,6 +469,7 @@ def _order_out(order: LicenseOrder) -> OrderOut:
created_at=order.created_at,
reviewed_at=order.reviewed_at,
sn=order.node.sn if order.node is not None else None,
display_node_id=display_node_id(order.node.node_index) if order.node is not None else None,
)