"""提现申请。可用余额 = 已入账积分 - 待处理提现 - 已完成提现。""" 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