feat(web): add fan date range modes and delta column

This commit is contained in:
kairee committed 2026-11-01 02:36:48 +08:00
1 parent 998d0fd0c0
commit 274eccba93
6 files changed
+253 -22

No files matched your search

+120 -15
View File
@@ -7,6 +7,7 @@
from __future__ import annotations
import calendar
import json
import math
import sqlite3
@@ -89,6 +90,84 @@ def _safe_float(value: Any) -> float | None:
return None
def _with_fan_deltas(
rows: Sequence[Mapping[str, Any]], recent_start: str
) -> list[dict[str, Any]]:
"""给最近记录附加“相较上一成功日期”的增量。"""
result: list[dict[str, Any]] = []
last_ok: int | None = None
for row in rows:
day = str(row["stat_date"])
ok = int(row.get("ok") or 0) == 1
delta: int | None = None
if ok:
follower = int(row.get("follower") or 0)
if last_ok is not None:
delta = follower - last_ok
last_ok = follower
if day >= recent_start:
result.append({**dict(row), "delta": delta})
return result
def _daily_fan_series(
rows: Sequence[Mapping[str, Any]], start: str, end: str
) -> tuple[list[str], list[int | None]]:
by_date = {str(row["stat_date"]): row for row in rows}
dates = _date_range(start, end)
series: list[int | None] = []
for day in dates:
row = by_date.get(day)
if row is None or int(row.get("ok") or 0) != 1:
series.append(None)
else:
series.append(int(row["follower"] or 0))
return dates, series
def _monthly_peak_series(
rows: Sequence[Mapping[str, Any]], start: str, end: str
) -> tuple[list[str], list[int | None]]:
"""超过一年时按自然月聚合,每月取粉丝峰值所在的日期。"""
start_day = date.fromisoformat(start)
end_day = date.fromisoformat(end)
peaks: dict[tuple[int, int], tuple[str, int]] = {}
for row in rows:
if int(row.get("ok") or 0) != 1:
continue
day = date.fromisoformat(str(row["stat_date"]))
key = (day.year, day.month)
value = int(row["follower"] or 0)
current = peaks.get(key)
if current is None or value > current[1] or (
value == current[1] and str(row["stat_date"]) > current[0]
):
peaks[key] = (str(row["stat_date"]), value)
dates: list[str] = []
series: list[int | None] = []
year, month = start_day.year, start_day.month
while (year, month) <= (end_day.year, end_day.month):
peak = peaks.get((year, month))
if peak is not None:
dates.append(peak[0])
series.append(peak[1])
else:
last_day = calendar.monthrange(year, month)[1]
placeholder = date(year, month, last_day)
if placeholder < start_day:
placeholder = start_day
if placeholder > end_day:
placeholder = end_day
dates.append(placeholder.isoformat())
series.append(None)
month += 1
if month > 12:
month = 1
year += 1
return dates, series
class DashboardQueries:
def __init__(
self,
@@ -899,12 +978,22 @@ class DashboardQueries:
mid: int | None = None,
*,
days: int = 90,
start: str | None = None,
end: str | None = None,
recent_days: int = 14,
) -> dict[str, Any]:
if days < 1:
raise ValueError("days 必须大于 0")
if recent_days < 1 or recent_days > 3650:
raise ValueError("recent_days 必须在 1~3650 之间")
for label, value in (("start", start), ("end", end)):
if value:
try:
date.fromisoformat(value)
except ValueError as exc:
raise ValueError(f"{label} 必须是 YYYY-MM-DD") from exc
if start and end and start > end:
raise ValueError("start 不能晚于 end")
def run(conn: sqlite3.Connection) -> dict[str, Any]:
cfg = self.config_store.get()
@@ -925,7 +1014,8 @@ class DashboardQueries:
"mid": None, "name": None, "available": False,
"options": options, "dates": [], "series": [],
"points": [], "recent_points": [], "days": days,
"recent_days": recent_days,
"mode": "daily", "range_start": None, "range_end": None,
"span_days": 0, "recent_days": recent_days,
}
rows = [
dict(row) for row in conn.execute(
@@ -944,24 +1034,35 @@ class DashboardQueries:
"points": [],
"recent_points": [],
"days": days,
"recent_days": recent_days,
"mode": "daily", "range_start": None, "range_end": None,
"span_days": 0, "recent_days": recent_days,
}
latest = max(str(row["stat_date"]) for row in rows if int(row["ok"]) == 1) if any(int(row["ok"]) == 1 for row in rows) else str(rows[-1]["stat_date"])
first_date = str(rows[0]["stat_date"])
recent_start = _date_minus(latest, recent_days - 1)
recent_points = [
row for row in rows if str(row["stat_date"]) >= recent_start
recent_points = _with_fan_deltas(rows, recent_start)
if start or end:
range_start = max(str(start or first_date), first_date)
range_end = min(str(end or latest), latest)
if range_start > range_end:
raise ValueError("所选日期范围内没有粉丝数据")
else:
range_start = max(_date_minus(latest, days - 1), first_date)
range_end = latest
span_days = (
date.fromisoformat(range_end) - date.fromisoformat(range_start)
).days + 1
mode = "daily" if span_days <= 366 else "monthly"
points = [
row for row in rows
if range_start <= str(row["stat_date"]) <= range_end
]
start = max(_date_minus(latest, days - 1), first_date)
dates = _date_range(start, latest)
by_date = {str(row["stat_date"]): row for row in rows}
series = []
for day in dates:
row = by_date.get(day)
if row is None or int(row["ok"]) != 1:
series.append(None)
else:
series.append(row["follower"])
if mode == "daily":
dates, series = _daily_fan_series(points, range_start, range_end)
else:
dates, series = _monthly_peak_series(
points, range_start, range_end
)
ok_rows = [row for row in rows if int(row["ok"]) == 1]
current = ok_rows[-1] if ok_rows else rows[-1]
previous = ok_rows[-2] if len(ok_rows) >= 2 else None
@@ -985,9 +1086,13 @@ class DashboardQueries:
"available": bool(ok_rows),
"options": options,
"days": days,
"mode": mode,
"range_start": range_start,
"range_end": range_end,
"span_days": span_days,
"dates": dates,
"series": series,
"points": rows,
"points": points,
"recent_points": recent_points,
"recent_days": recent_days,
"stat_date": current["stat_date"],