"""对外 HTTP 接口。业务规则在 services 中,这里只做参数转发。""" from datetime import date, timedelta from typing import Annotated from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from app.clock import utcnow from app.database import get_db from app.models import ComputeTask from app.schemas import ( BalanceOut, ContributionOut, HeartbeatIn, HeartbeatOut, NodeCreate, NodeOut, SettleOut, TaskOut, TaskReportIn, UserCreate, UserOut, WithdrawalConfirm, WithdrawalCreate, WithdrawalOut, WithdrawalReject, YieldOut, ) from app.services.common import get_node_by_sn, get_user_by_wallet from app.services.heartbeat import record_heartbeat 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 router = APIRouter(prefix="/api/v1") Db = Annotated[Session, Depends(get_db)] @router.post("/users", response_model=UserOut) def create_user(body: UserCreate, db: Db) -> UserOut: return register_user(db, body.wallet_address) @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) @router.get("/nodes/{sn}", response_model=NodeOut) def get_node(sn: str, db: Db) -> NodeOut: return get_node_by_sn(db, sn) @router.post("/node/heartbeat", response_model=HeartbeatOut) def heartbeat(body: HeartbeatIn, db: Db) -> HeartbeatOut: node = record_heartbeat(db, body.sn) return HeartbeatOut( sn=node.sn, status=node.status, last_heartbeat_at=node.last_heartbeat_at, server_time=node.last_heartbeat_at, ) @router.get("/node/{sn}/tasks", response_model=list[TaskOut]) def pending_tasks(sn: str, db: Db) -> list[TaskOut]: 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: return _task_out(report_task(db, body.sn, body.task_id, body.success, body.nonce)) @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) 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: target = yield_date or (utcnow().date() - timedelta(days=1)) summary = settle_yield_date(db, target) return SettleOut( yield_date=summary.yield_date, created=summary.created, skipped=summary.skipped, total_points=summary.total_points, is_partial=summary.is_partial, ) @router.get("/nodes/{sn}/yields", response_model=list[YieldOut]) def yields(sn: str, db: Db) -> list[YieldOut]: return list_node_yields(db, sn) @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)) @router.post("/withdrawals", response_model=WithdrawalOut) def create_withdrawal(body: WithdrawalCreate, db: Db) -> WithdrawalOut: 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: 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: return reject_withdrawal(db, withdrawal_id, body.note) def _task_out(task: ComputeTask) -> TaskOut: return TaskOut( task_id=task.id, task_type=task.task_type, payload=task.payload, status=task.status, dispatched_at=task.dispatched_at, deadline_at=task.deadline_at, )