Reapply "Redesign finance dashboard with period summaries"

This reverts commit da14ec5740.
This commit is contained in:
SmartUp Developer
2026-07-03 23:25:47 +08:00
parent da14ec5740
commit 4dd4448970
6 changed files with 1207 additions and 801 deletions
+183
View File
@@ -449,6 +449,189 @@ def _load_summary(row: Any) -> dict[str, Any]:
return data
def _date_range(start: date, end: date) -> list[date]:
"""Return every date in [start, end]."""
days = (end - start).days
return [start + timedelta(days=i) for i in range(days + 1)]
def _month_end(target_date: date) -> date:
"""Return the last day of target_date's natural month."""
if target_date.month == 12:
next_month = date(target_date.year + 1, 1, 1)
else:
next_month = date(target_date.year, target_date.month + 1, 1)
return next_month - timedelta(days=1)
def finance_period_bounds(period: str, target_date: date) -> tuple[date, date]:
"""Resolve finance summary period bounds."""
if period == "day":
return target_date, target_date
if period == "week":
return target_date - timedelta(days=6), target_date
if period == "month":
return date(target_date.year, target_date.month, 1), _month_end(target_date)
raise ValueError(f"unsupported finance period: {period}")
def _chart_from_summaries(
dates: list[date],
summaries_by_date: dict[date, dict[str, Any]],
) -> dict[str, list[Any]]:
"""Build date-aligned chart series, using zero for dates without snapshots."""
return {
"labels": [d.isoformat() for d in dates],
"revenue": [
round(float(summaries_by_date.get(d, {}).get("total_revenue", 0.0) or 0.0), 6)
for d in dates
],
"cost": [
round(float(summaries_by_date.get(d, {}).get("total_cost", 0.0) or 0.0), 6)
for d in dates
],
}
def _aggregate_items(rows: list[tuple[date, dict[str, Any]]], side: str) -> list[dict[str, Any]]:
"""Aggregate item amounts by (id, upstream_type); any daily failure fails the item."""
grouped: dict[tuple[int | str, str], dict[str, Any]] = {}
for row_date, summary in rows:
for item in summary.get(side, []) or []:
ident = item.get("id")
if ident is None:
ident = -1
upstream_type = item.get("upstream_type") or ""
key = (ident, upstream_type)
aggregate = grouped.setdefault(
key,
{
"id": ident,
"name": item.get("name", ""),
"upstream_type": upstream_type,
"amount": 0.0,
"status": "success",
"error": None,
"_errors": [],
},
)
if item.get("name"):
aggregate["name"] = item["name"]
aggregate["amount"] += float(item.get("amount") or 0.0)
if item.get("status") == "failed":
aggregate["status"] = "failed"
error = item.get("error") or "统计失败"
aggregate["_errors"].append(f"{row_date.isoformat()}: {error}")
result = []
for item in grouped.values():
errors = item.pop("_errors")
item["amount"] = round(item["amount"], 6)
item["error"] = "".join(errors) if errors else None
result.append(item)
return sorted(result, key=lambda i: (i["status"] != "failed", str(i.get("name") or ""), str(i.get("id"))))
def get_stored_period_summary(
db: Session,
period: str,
target_date: date,
) -> dict[str, Any]:
"""Aggregate already-stored finance daily snapshots for week/month periods.
This intentionally does not compute, backfill, or overwrite daily summaries.
Missing dates are reported through partial metadata and charted as zero.
"""
from app.models.finance_daily_summary import FinanceDailySummary
start_date, end_date = finance_period_bounds(period, target_date)
all_dates = _date_range(start_date, end_date)
rows = (
db.query(FinanceDailySummary)
.filter(
FinanceDailySummary.stat_date >= start_date,
FinanceDailySummary.stat_date <= end_date,
)
.order_by(FinanceDailySummary.stat_date.asc())
.all()
)
loaded_rows: list[tuple[date, dict[str, Any]]] = []
summaries_by_date: dict[date, dict[str, Any]] = {}
for row in rows:
summary = _load_summary(row)
loaded_rows.append((row.stat_date, summary))
summaries_by_date[row.stat_date] = summary
missing_dates = [d.isoformat() for d in all_dates if d not in summaries_by_date]
total_revenue = sum(float(s.get("total_revenue", 0.0) or 0.0) for s in summaries_by_date.values())
total_cost = sum(float(s.get("total_cost", 0.0) or 0.0) for s in summaries_by_date.values())
net = total_revenue - total_cost
margin = (net / total_revenue * 100) if total_revenue > 0 else 0.0
website_items = _aggregate_items(loaded_rows, "website_items")
upstream_items = _aggregate_items(loaded_rows, "upstream_items")
failed_count = sum(1 for i in website_items + upstream_items if i["status"] == "failed")
return {
"period": period,
"date": target_date.isoformat(),
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
"total_revenue": round(total_revenue, 6),
"total_cost": round(total_cost, 6),
"net_income": round(net, 6),
"margin_percent": round(margin, 2),
"website_items": website_items,
"upstream_items": upstream_items,
"failed_count": failed_count,
"success": failed_count == 0,
"chart": _chart_from_summaries(all_dates, summaries_by_date),
"missing_dates": missing_dates,
"included_days": len(summaries_by_date),
"partial": len(missing_dates) > 0,
"from_snapshot": True,
}
def get_finance_summary(
db: Session,
period: str,
target_date: date,
) -> dict[str, Any]:
"""Return day/week/month finance summary with date-aligned chart metadata."""
if period == "day":
summary = get_or_create_daily_summary(db, target_date)
chart_start = target_date - timedelta(days=6)
chart_dates = _date_range(chart_start, target_date)
from app.models.finance_daily_summary import FinanceDailySummary
rows = (
db.query(FinanceDailySummary)
.filter(
FinanceDailySummary.stat_date >= chart_start,
FinanceDailySummary.stat_date <= target_date,
)
.order_by(FinanceDailySummary.stat_date.asc())
.all()
)
summaries_by_date = {row.stat_date: _load_summary(row) for row in rows}
result = dict(summary)
result.update({
"period": period,
"start_date": target_date.isoformat(),
"end_date": target_date.isoformat(),
"chart": _chart_from_summaries(chart_dates, summaries_by_date),
"missing_dates": [],
"included_days": 1,
"partial": False,
})
return result
return get_stored_period_summary(db, period, target_date)
def get_or_create_daily_summary(
db: Session,
target_date: date,