"""DePIN 控制面数据模型。 核心表: - User:用户与钱包地址 - ComputeNode:算力节点、授权金额、12 个月授权周期、在线状态 - NodeYield:每日算力产出与积分流水 - WithdrawalLog:数字资产提现申请、链上哈希与状态 支撑表(业务计算所依赖,不单独对外当作资产账本): - UptimeInterval:一次连续在线区间,用来计算有效在线时长 - ComputeTask:下发给节点的模拟计算任务及回传校验结果 - SchedulerLock:多实例部署时,定时任务靠数据库互斥,避免重复下发和重复清算 """ from __future__ import annotations import enum from datetime import date, datetime from decimal import Decimal from sqlalchemy import ( CheckConstraint, Enum, ForeignKey, Index, Integer, Numeric, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.types import JSON, TypeDecorator from app.clock import utcnow from app.database import Base, UtcDateTime from app.money import money class PreciseDecimal(TypeDecorator): """金额与比率。SQLite 会把 Numeric 读成 float,这里统一还原成 8 位小数。""" impl = Numeric(24, 8) cache_ok = True def process_bind_param(self, value, dialect): if value is None: return None return money(value) def process_result_value(self, value, dialect): if value is None: return None return money(value) MONEY = PreciseDecimal() RATE = PreciseDecimal() def _enum(enum_cls: type[enum.Enum], length: int = 16) -> Enum: """把枚举的 value(online/offline)写入数据库,而不是成员名。""" return Enum( enum_cls, native_enum=False, length=length, values_callable=lambda obj: [item.value for item in obj], ) class NodeStatus(str, enum.Enum): ONLINE = "online" OFFLINE = "offline" class YieldStatus(str, enum.Enum): SETTLED = "settled" VOID = "void" class WithdrawalStatus(str, enum.Enum): PENDING = "pending" COMPLETED = "completed" REJECTED = "rejected" FAILED = "failed" class TaskStatus(str, enum.Enum): PENDING = "pending" COMPLETED = "completed" FAILED = "failed" EXPIRED = "expired" class PushStatus(str, enum.Enum): QUEUED = "queued" PUSHED = "pushed" FAILED = "failed" class User(Base): """用户。一个钱包地址对应一名用户,可绑定多台算力节点。""" __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) wallet_address: Mapped[str] = mapped_column(String(128), unique=True, index=True) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=utcnow) nodes: Mapped[list[ComputeNode]] = relationship(back_populates="user") yields: Mapped[list[NodeYield]] = relationship(back_populates="user") withdrawals: Mapped[list[WithdrawalLog]] = relationship(back_populates="user") class ComputeNode(Base): """算力节点。购买金额在授权周期内按天线性释放,实际到账再乘当日表现系数。""" __tablename__ = "compute_nodes" __table_args__ = ( CheckConstraint("purchase_amount > 0", name="ck_node_purchase_positive"), CheckConstraint("license_months > 0", name="ck_node_license_months"), Index("ix_node_status_heartbeat", "status", "last_heartbeat_at"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True) sn: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="节点唯一序列号") purchase_amount: Mapped[Decimal] = mapped_column(MONEY, comment="授权购买金额") license_months: Mapped[int] = mapped_column(Integer, default=12) licensed_at: Mapped[datetime] = mapped_column(UtcDateTime, comment="授权开始时间") license_expires_at: Mapped[datetime] = mapped_column(UtcDateTime, comment="授权结束时间") status: Mapped[NodeStatus] = mapped_column(_enum(NodeStatus), default=NodeStatus.OFFLINE, index=True) last_heartbeat_at: Mapped[datetime | None] = mapped_column(UtcDateTime) callback_url: Mapped[str | None] = mapped_column(String(512), comment="可选的任务推送地址") created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=utcnow) updated_at: Mapped[datetime] = mapped_column(UtcDateTime, default=utcnow, onupdate=utcnow) user: Mapped[User] = relationship(back_populates="nodes") yields: Mapped[list[NodeYield]] = relationship(back_populates="node") tasks: Mapped[list[ComputeTask]] = relationship(back_populates="node") uptime_intervals: Mapped[list[UptimeInterval]] = relationship(back_populates="node") class UptimeInterval(Base): """一段连续在线时间。心跳首次上线时打开,超时或授权到期时关闭。""" __tablename__ = "uptime_intervals" __table_args__ = (Index("ix_uptime_node_window", "node_id", "started_at", "ended_at"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) node_id: Mapped[int] = mapped_column(ForeignKey("compute_nodes.id"), index=True) started_at: Mapped[datetime] = mapped_column(UtcDateTime) ended_at: Mapped[datetime | None] = mapped_column(UtcDateTime) close_reason: Mapped[str | None] = mapped_column(String(32)) node: Mapped[ComputeNode] = relationship(back_populates="uptime_intervals") class ComputeTask(Base): """下发给在线节点的模拟计算任务。客户端须回传 task_id 与一次性 nonce。""" __tablename__ = "compute_tasks" __table_args__ = ( Index("ix_task_node_status", "node_id", "status"), Index("ix_task_dispatched_at", "dispatched_at"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, comment="task_id") node_id: Mapped[int] = mapped_column(ForeignKey("compute_nodes.id"), index=True) task_type: Mapped[str] = mapped_column(String(32), default="simulated_compute") payload: Mapped[dict] = mapped_column(JSON) status: Mapped[TaskStatus] = mapped_column(_enum(TaskStatus), default=TaskStatus.PENDING, index=True) push_status: Mapped[PushStatus] = mapped_column(_enum(PushStatus), default=PushStatus.QUEUED) push_error: Mapped[str | None] = mapped_column(String(255)) dispatched_at: Mapped[datetime] = mapped_column(UtcDateTime) deadline_at: Mapped[datetime] = mapped_column(UtcDateTime) responded_at: Mapped[datetime | None] = mapped_column(UtcDateTime) success: Mapped[bool | None] = mapped_column() node: Mapped[ComputeNode] = relationship(back_populates="tasks") class NodeYield(Base): """每日产出流水。同一节点同一天只有一条结算记录。""" __tablename__ = "node_yields" __table_args__ = ( UniqueConstraint("node_id", "yield_date", name="uq_node_yield_date"), Index("ix_node_yield_user_date", "user_id", "yield_date"), CheckConstraint("points_delta >= 0", name="ck_yield_points_non_negative"), CheckConstraint("compute_output >= 0", name="ck_yield_output_non_negative"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) node_id: Mapped[int] = mapped_column(ForeignKey("compute_nodes.id"), index=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True) yield_date: Mapped[date] = mapped_column(index=True, comment="清算日,UTC") compute_output: Mapped[Decimal] = mapped_column(MONEY, comment="当日有效算力产出,等于授权金额乘表现系数") points_delta: Mapped[Decimal] = mapped_column(MONEY, comment="当日入账积分") online_seconds: Mapped[int] = mapped_column(Integer, default=0) eligible_seconds: Mapped[int] = mapped_column(Integer, default=0) online_rate: Mapped[Decimal] = mapped_column(RATE, default=0) tasks_dispatched: Mapped[int] = mapped_column(Integer, default=0) tasks_completed: Mapped[int] = mapped_column(Integer, default=0) task_response_rate: Mapped[Decimal] = mapped_column(RATE, default=0) online_weight: Mapped[Decimal] = mapped_column(RATE, default=0) task_weight: Mapped[Decimal] = mapped_column(RATE, default=0) performance: Mapped[Decimal] = mapped_column(RATE, default=0, comment="当日表现系数,0 到 1") cycle_days: Mapped[int] = mapped_column(Integer) status: Mapped[YieldStatus] = mapped_column(_enum(YieldStatus), default=YieldStatus.SETTLED) is_partial: Mapped[bool] = mapped_column(default=False, comment="结算时该 UTC 日尚未结束") created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=utcnow) node: Mapped[ComputeNode] = relationship(back_populates="yields") user: Mapped[User] = relationship(back_populates="yields") class WithdrawalLog(Base): """提现申请。待处理和已完成的金额都会占用可用积分,拒绝后释放。""" __tablename__ = "withdrawal_logs" __table_args__ = ( Index("ix_withdrawal_user_status", "user_id", "status"), CheckConstraint("amount > 0", name="ck_withdrawal_amount_positive"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True) amount: Mapped[Decimal] = mapped_column(MONEY) asset_symbol: Mapped[str] = mapped_column(String(32), default="POINT") tx_hash: Mapped[str | None] = mapped_column(String(128), unique=True) status: Mapped[WithdrawalStatus] = mapped_column( _enum(WithdrawalStatus), default=WithdrawalStatus.PENDING, index=True, ) note: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=utcnow) updated_at: Mapped[datetime] = mapped_column(UtcDateTime, default=utcnow, onupdate=utcnow) user: Mapped[User] = relationship(back_populates="withdrawals") class SchedulerLock(Base): """定时任务锁。锁到期后其他实例才能接手,进程崩溃不会永久占住任务。""" __tablename__ = "scheduler_locks" name: Mapped[str] = mapped_column(String(64), primary_key=True) locked_until: Mapped[datetime] = mapped_column(UtcDateTime) owner: Mapped[str] = mapped_column(String(128))