2bbdb00a4d
包含节点账本、每日清算,以及 Ubuntu 交互式部署脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""提现申请。可用余额 = 已入账积分 - 待处理提现 - 已完成提现。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.clock import utcnow
|
|
from app.errors import AppError
|
|
from app.models import NodeYield, WithdrawalLog, WithdrawalStatus, YieldStatus
|
|
from app.money import money, to_decimal
|
|
from app.services.common import get_user_by_wallet
|
|
|
|
|
|
def available_balance(session: Session, user_id: int) -> Decimal:
|
|
credited_rows = session.scalars(
|
|
select(NodeYield.points_delta).where(
|
|
NodeYield.user_id == user_id,
|
|
NodeYield.status == YieldStatus.SETTLED,
|
|
)
|
|
).all()
|
|
reserved_rows = session.scalars(
|
|
select(WithdrawalLog.amount).where(
|
|
WithdrawalLog.user_id == user_id,
|
|
WithdrawalLog.status.in_([WithdrawalStatus.PENDING, WithdrawalStatus.COMPLETED]),
|
|
)
|
|
).all()
|
|
credited = sum((to_decimal(item) for item in credited_rows), Decimal("0"))
|
|
reserved = sum((to_decimal(item) for item in reserved_rows), Decimal("0"))
|
|
balance = credited - reserved
|
|
if balance <= 0:
|
|
return Decimal("0")
|
|
return money(balance)
|
|
|
|
|
|
def request_withdrawal(
|
|
session: Session,
|
|
wallet_address: str,
|
|
amount: Decimal,
|
|
asset_symbol: str = "POINT",
|
|
) -> tuple[WithdrawalLog, Decimal]:
|
|
user = get_user_by_wallet(session, wallet_address.strip())
|
|
value = money(amount)
|
|
if value <= 0:
|
|
raise AppError("提现金额必须大于 0")
|
|
balance = available_balance(session, user.id)
|
|
if value > balance:
|
|
raise AppError("可用积分不足")
|
|
row = WithdrawalLog(
|
|
user_id=user.id,
|
|
amount=value,
|
|
asset_symbol=asset_symbol.strip().upper() or "POINT",
|
|
status=WithdrawalStatus.PENDING,
|
|
created_at=utcnow(),
|
|
updated_at=utcnow(),
|
|
)
|
|
session.add(row)
|
|
session.flush()
|
|
return row, money(balance - value)
|
|
|
|
|
|
def confirm_withdrawal(session: Session, withdrawal_id: int, tx_hash: str) -> WithdrawalLog:
|
|
row = _get_withdrawal(session, withdrawal_id)
|
|
if row.status != WithdrawalStatus.PENDING:
|
|
raise AppError("仅待处理的提现可以确认", 409)
|
|
digest = tx_hash.strip()
|
|
occupied = session.scalar(select(WithdrawalLog.id).where(WithdrawalLog.tx_hash == digest))
|
|
if occupied is not None:
|
|
raise AppError("交易哈希已存在", 409)
|
|
row.tx_hash = digest
|
|
row.status = WithdrawalStatus.COMPLETED
|
|
row.updated_at = utcnow()
|
|
try:
|
|
session.flush()
|
|
except IntegrityError as exc:
|
|
session.rollback()
|
|
raise AppError("交易哈希已存在", 409) from exc
|
|
return row
|
|
|
|
|
|
def reject_withdrawal(session: Session, withdrawal_id: int, note: str | None = None) -> WithdrawalLog:
|
|
row = _get_withdrawal(session, withdrawal_id)
|
|
if row.status != WithdrawalStatus.PENDING:
|
|
raise AppError("仅待处理的提现可以拒绝", 409)
|
|
row.status = WithdrawalStatus.REJECTED
|
|
row.note = note
|
|
row.updated_at = utcnow()
|
|
session.flush()
|
|
return row
|
|
|
|
|
|
def _get_withdrawal(session: Session, withdrawal_id: int) -> WithdrawalLog:
|
|
row = session.get(WithdrawalLog, withdrawal_id)
|
|
if row is None:
|
|
raise AppError("提现记录不存在", 404)
|
|
return row
|