fix finance balance delta recharges
This commit is contained in:
@@ -4,18 +4,19 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date as date_type, datetime, timezone
|
||||
from typing import Any, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query as QueryParam, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import SessionLocal, get_db
|
||||
from app.models.admin_user import AdminUser
|
||||
from app.models.upstream import Upstream
|
||||
from app.models.upstream_key import UpstreamGeneratedKey
|
||||
from app.models.upstream_recharge_event import UpstreamRechargeEvent
|
||||
from app.models.snapshot import UpstreamRateSnapshot
|
||||
from app.schemas.upstream import (
|
||||
GenerateKeysByGroupsRequest,
|
||||
@@ -23,10 +24,12 @@ from app.schemas.upstream import (
|
||||
GeneratedUpstreamKeyResponse,
|
||||
UpstreamCreate, UpstreamUpdate, UpstreamResponse, SnapshotResponse, TestResult,
|
||||
UpstreamBatchActionItem, UpstreamBatchActionSummary, UpstreamBatchActionResponse,
|
||||
UpstreamRechargeCreate, UpstreamRechargeUpdate, UpstreamRechargeResponse,
|
||||
)
|
||||
from app.services.upstream_client import UpstreamClient, UpstreamError, _PendingKeyError, build_snapshot, build_new_api_token_name, mask_secret, _extract_key_value
|
||||
from app.services.auth_config import MASK, mask_auth_config, normalize_auth_config
|
||||
from app.services.snapshot_service import diff_snapshots
|
||||
from app.services.finance_service import today_shanghai
|
||||
from app.services.snapshot_service import diff_snapshots, write_balance_snapshot
|
||||
from app.services import scheduler as sched_svc
|
||||
from app.services import webhook_service
|
||||
from app.services import website_sync
|
||||
@@ -138,11 +141,29 @@ def _to_response(u: Upstream) -> UpstreamResponse:
|
||||
balance_response_path=u.balance_response_path or "",
|
||||
balance_divisor=u.balance_divisor or 1.0,
|
||||
balance_alert_threshold=u.balance_alert_threshold,
|
||||
finance_cost_mode=u.finance_cost_mode or "usage_stats",
|
||||
created_at=u.created_at,
|
||||
updated_at=u.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _recharge_response(row: UpstreamRechargeEvent) -> UpstreamRechargeResponse:
|
||||
return UpstreamRechargeResponse(
|
||||
id=row.id,
|
||||
upstream_id=row.upstream_id,
|
||||
date=row.recharge_date,
|
||||
amount=row.amount,
|
||||
note=row.note or "",
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _validate_recharge_date(value: date_type) -> None:
|
||||
if value > today_shanghai():
|
||||
raise HTTPException(422, "date 不允许是未来日期")
|
||||
|
||||
|
||||
@router.get("", response_model=List[UpstreamResponse])
|
||||
def list_upstreams(db: Session = Depends(get_db), _=Depends(get_current_user)):
|
||||
return [_to_response(u) for u in db.query(Upstream).order_by(Upstream.id).all()]
|
||||
@@ -694,6 +715,7 @@ def _test_upstream_core(db: Session, u: Upstream) -> UpstreamBatchActionItem:
|
||||
raw_balance = client.get_balance(u.balance_endpoint, u.balance_response_path)
|
||||
if raw_balance is not None:
|
||||
u.balance = raw_balance / (u.balance_divisor or 1.0)
|
||||
write_balance_snapshot(db, u.id, u.balance)
|
||||
u.balance_updated_at = datetime.now(timezone.utc) if raw_balance is not None else None
|
||||
except Exception as exc:
|
||||
logger.warning("test-all: upstream %s balance failed: %s", u.name, exc)
|
||||
@@ -738,6 +760,7 @@ def _check_now_core(db: Session, u: Upstream) -> tuple[str, bool]:
|
||||
raw_balance = client.get_balance(u.balance_endpoint, u.balance_response_path)
|
||||
if raw_balance is not None:
|
||||
u.balance = raw_balance / (u.balance_divisor or 1.0)
|
||||
write_balance_snapshot(db, u.id, u.balance)
|
||||
u.balance_updated_at = datetime.now(timezone.utc) if raw_balance is not None else None
|
||||
except Exception as exc:
|
||||
logger.warning("check-now: upstream %s balance failed: %s", u.name, exc)
|
||||
@@ -919,6 +942,7 @@ def create_upstream(
|
||||
balance_response_path=body.balance_response_path,
|
||||
balance_divisor=body.balance_divisor,
|
||||
balance_alert_threshold=body.balance_alert_threshold,
|
||||
finance_cost_mode=body.finance_cost_mode,
|
||||
)
|
||||
db.add(u)
|
||||
db.commit()
|
||||
@@ -935,6 +959,99 @@ def get_upstream(uid: int, db: Session = Depends(get_db), _=Depends(get_current_
|
||||
return _to_response(u)
|
||||
|
||||
|
||||
@router.get("/{uid}/recharges", response_model=List[UpstreamRechargeResponse])
|
||||
def list_recharges(
|
||||
uid: int,
|
||||
date: date_type | None = QueryParam(None),
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
):
|
||||
u = db.query(Upstream).filter(Upstream.id == uid).first()
|
||||
if not u:
|
||||
raise HTTPException(404, "upstream not found")
|
||||
query = db.query(UpstreamRechargeEvent).filter(UpstreamRechargeEvent.upstream_id == uid)
|
||||
if date is not None:
|
||||
query = query.filter(UpstreamRechargeEvent.recharge_date == date)
|
||||
rows = query.order_by(UpstreamRechargeEvent.recharge_date.desc(), UpstreamRechargeEvent.id.desc()).all()
|
||||
return [_recharge_response(row) for row in rows]
|
||||
|
||||
|
||||
@router.post("/{uid}/recharges", response_model=UpstreamRechargeResponse, status_code=201)
|
||||
def create_recharge(
|
||||
uid: int,
|
||||
body: UpstreamRechargeCreate,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
):
|
||||
u = db.query(Upstream).filter(Upstream.id == uid).first()
|
||||
if not u:
|
||||
raise HTTPException(404, "upstream not found")
|
||||
_validate_recharge_date(body.date)
|
||||
row = UpstreamRechargeEvent(
|
||||
upstream_id=uid,
|
||||
recharge_date=body.date,
|
||||
amount=body.amount,
|
||||
note=body.note or "",
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return _recharge_response(row)
|
||||
|
||||
|
||||
@router.put("/{uid}/recharges/{rid}", response_model=UpstreamRechargeResponse)
|
||||
def update_recharge(
|
||||
uid: int,
|
||||
rid: int,
|
||||
body: UpstreamRechargeUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
):
|
||||
u = db.query(Upstream).filter(Upstream.id == uid).first()
|
||||
if not u:
|
||||
raise HTTPException(404, "upstream not found")
|
||||
row = (
|
||||
db.query(UpstreamRechargeEvent)
|
||||
.filter(UpstreamRechargeEvent.id == rid, UpstreamRechargeEvent.upstream_id == uid)
|
||||
.first()
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(404, "recharge event not found")
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
if "date" in data:
|
||||
_validate_recharge_date(data["date"])
|
||||
row.recharge_date = data["date"]
|
||||
if "amount" in data:
|
||||
row.amount = data["amount"]
|
||||
if "note" in data:
|
||||
row.note = data["note"] or ""
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return _recharge_response(row)
|
||||
|
||||
|
||||
@router.delete("/{uid}/recharges/{rid}", status_code=204)
|
||||
def delete_recharge(
|
||||
uid: int,
|
||||
rid: int,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
):
|
||||
u = db.query(Upstream).filter(Upstream.id == uid).first()
|
||||
if not u:
|
||||
raise HTTPException(404, "upstream not found")
|
||||
row = (
|
||||
db.query(UpstreamRechargeEvent)
|
||||
.filter(UpstreamRechargeEvent.id == rid, UpstreamRechargeEvent.upstream_id == uid)
|
||||
.first()
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(404, "recharge event not found")
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.put("/{uid}", response_model=UpstreamResponse)
|
||||
def update_upstream(
|
||||
uid: int,
|
||||
@@ -1029,8 +1146,6 @@ def latest_snapshot(uid: int, db: Session = Depends(get_db), _=Depends(get_curre
|
||||
)
|
||||
|
||||
|
||||
from fastapi import Query as QueryParam
|
||||
|
||||
@router.get("/{uid}/snapshots", response_model=List[SnapshotResponse])
|
||||
def list_snapshots(
|
||||
uid: int,
|
||||
|
||||
Reference in New Issue
Block a user