fix finance balance delta recharges

This commit is contained in:
SmartUp Developer
2026-07-03 20:05:28 +08:00
parent bb7ed5a7e0
commit 1670503720
12 changed files with 745 additions and 24 deletions
+109 -10
View File
@@ -21,10 +21,13 @@ from typing import Any
import pytz
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.models.upstream import Upstream
from app.models.website import Website
from app.models.snapshot import UpstreamBalanceSnapshot
from app.models.upstream_recharge_event import UpstreamRechargeEvent
from app.services.upstream_client import UpstreamClient
from app.services.website_client import Sub2ApiWebsiteClient, WebsiteError
@@ -81,6 +84,13 @@ def date_to_shanghai_timestamps(d: date) -> tuple[int, int]:
return int(start.timestamp()), int(end.timestamp())
def date_to_shanghai_utc_range(d: date) -> tuple[datetime, datetime]:
"""Return UTC datetimes for [start, next_day_start) of a Shanghai calendar day."""
start = SHANGHAI_TZ.localize(datetime(d.year, d.month, d.day, 0, 0, 0))
end = start + timedelta(days=1)
return start.astimezone(timezone.utc), end.astimezone(timezone.utc)
# ─────────────────────────────────────────────
# Website revenue
# ─────────────────────────────────────────────
@@ -229,6 +239,86 @@ def fetch_upstream_cost_new_api(upstream: Upstream, target_date: date) -> tuple[
return 0.0, str(e)
# ─────────────────────────────────────────────
# Balance-delta cost (local balance snapshots)
# ─────────────────────────────────────────────
def fetch_upstream_cost_balance_delta(
upstream: Upstream,
target_date: date,
db: Session,
) -> tuple[float, str | None]:
"""Compute daily cost from local balance snapshots for balance_delta upstreams.
Algorithm:
1. Find the most recent snapshot BEFORE the start of target_date (baseline).
2. Find snapshots DURING target_date and use the last one as ending balance.
3. Sum manual recharge events for target_date.
4. cost = baseline + recharge_total - ending. Negative values fail.
"""
start_dt, end_dt = date_to_shanghai_utc_range(target_date)
# Baseline: latest snapshot before start of target date
baseline = (
db.query(UpstreamBalanceSnapshot)
.filter(
UpstreamBalanceSnapshot.upstream_id == upstream.id,
UpstreamBalanceSnapshot.captured_at < start_dt,
)
.order_by(UpstreamBalanceSnapshot.captured_at.desc())
.first()
)
# Intra-day snapshots during target date
intra_day = (
db.query(UpstreamBalanceSnapshot)
.filter(
UpstreamBalanceSnapshot.upstream_id == upstream.id,
UpstreamBalanceSnapshot.captured_at >= start_dt,
UpstreamBalanceSnapshot.captured_at < end_dt,
)
.order_by(UpstreamBalanceSnapshot.captured_at.asc())
.all()
)
if baseline is None:
return 0.0, "缺少目标日期前的余额基线样本,无法进行差分统计"
if not intra_day:
return 0.0, "目标日期内无余额样本,无法进行差分统计"
recharge_total = (
db.query(func.coalesce(func.sum(UpstreamRechargeEvent.amount), 0.0))
.filter(
UpstreamRechargeEvent.upstream_id == upstream.id,
UpstreamRechargeEvent.recharge_date == target_date,
)
.scalar()
)
recharge_total = float(recharge_total or 0.0)
max_balance_rise = 0.0
prev = float(baseline.balance)
for snap in intra_day:
current = float(snap.balance)
if current > prev:
max_balance_rise = max(max_balance_rise, current - prev)
prev = current
if max_balance_rise > 0 and recharge_total <= 0:
return 0.0, "检测到余额上涨,但当天无充值记录;请补录充值后重新对账"
if max_balance_rise > 0 and recharge_total + 1e-9 < max_balance_rise:
return 0.0, "当天充值总额小于观察到的最大余额上涨,充值金额可能漏填"
ending = float(intra_day[-1].balance)
total = float(baseline.balance) + recharge_total - ending
if total < -1e-9:
return 0.0, "余额差分计算为负数,请检查余额样本或补录充值记录"
return round(max(total, 0.0), 6), None
# ─────────────────────────────────────────────
# Orchestration
# ─────────────────────────────────────────────
@@ -237,6 +327,7 @@ def get_daily_summary(
websites: list[Website],
upstreams: list[Upstream],
target_date: date,
db: Session | None = None,
) -> dict[str, Any]:
"""Compute daily revenue vs cost summary.
@@ -264,16 +355,24 @@ def get_daily_summary(
total_revenue += amount
for u in upstreams:
utype = classify_upstream(u)
if utype == "sub2api":
amount, err = fetch_upstream_cost_sub2api(u, target_date)
elif utype == "new_api":
amount, err = fetch_upstream_cost_new_api(u, target_date)
cost_mode = getattr(u, "finance_cost_mode", "usage_stats")
if cost_mode == "balance_delta":
if db is None:
amount, err = 0.0, "余额差分统计需要数据库连接"
else:
amount, err = fetch_upstream_cost_balance_delta(u, target_date, db)
utype = classify_upstream(u)
else:
amount, err = 0.0, (
f"未知上游类型(auth_type={u.auth_type}, api_prefix={u.api_prefix}),"
"无法统计消费。请检查上游配置。"
)
utype = classify_upstream(u)
if utype == "sub2api":
amount, err = fetch_upstream_cost_sub2api(u, target_date)
elif utype == "new_api":
amount, err = fetch_upstream_cost_new_api(u, target_date)
else:
amount, err = 0.0, (
f"未知上游类型(auth_type={u.auth_type}, api_prefix={u.api_prefix}),"
"无法统计消费。请检查上游配置。"
)
item = {
"id": u.id,
@@ -339,7 +438,7 @@ def compute_daily_summary(
"""Compute a fresh daily summary (no DB write). Used for compare."""
websites = db.query(Website).filter(Website.enabled == True).all()
upstreams = db.query(Upstream).filter(Upstream.enabled == True).all()
return get_daily_summary(websites, upstreams, target_date)
return get_daily_summary(websites, upstreams, target_date, db=db)
def _load_summary(row: Any) -> dict[str, Any]: