feat: home growth auto(7→1) + hourly recent-video snapshot #2
No files matched your search
@@ -699,7 +699,7 @@ class DashboardQueries:
|
|||||||
sort: str = "heat",
|
sort: str = "heat",
|
||||||
within_days: int | None = None,
|
within_days: int | None = None,
|
||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
growth_days: int | str = 7,
|
growth_days: int | str = "auto",
|
||||||
folder_id: int | None = None,
|
folder_id: int | None = None,
|
||||||
q: str | None = None,
|
q: str | None = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
@@ -734,7 +734,7 @@ class DashboardQueries:
|
|||||||
sort: str = "heat",
|
sort: str = "heat",
|
||||||
within_days: int | None = None,
|
within_days: int | None = None,
|
||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
growth_days: int | str = 7,
|
growth_days: int | str = "auto",
|
||||||
folder_id: int | None = None,
|
folder_id: int | None = None,
|
||||||
q: str | None = None,
|
q: str | None = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
@@ -742,7 +742,10 @@ class DashboardQueries:
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if sort not in VIDEO_SORTS:
|
if sort not in VIDEO_SORTS:
|
||||||
raise ValueError(f"不支持的排序: {sort}")
|
raise ValueError(f"不支持的排序: {sort}")
|
||||||
|
# auto:仅增长类排序由服务端选窗(配置 home_growth_days;默认先 7 再降 1)。
|
||||||
|
# 首页应省略 growth_days,勿自行探查。其它排序把 auto 当作 7(兼容旧默认)。
|
||||||
if growth_days == "auto":
|
if growth_days == "auto":
|
||||||
|
if sort in {"growth", "growth_rate"}:
|
||||||
preferred = self.config_store.get().analytics.home_growth_days
|
preferred = self.config_store.get().analytics.home_growth_days
|
||||||
if preferred in (1, 7):
|
if preferred in (1, 7):
|
||||||
growth_days = int(preferred)
|
growth_days = int(preferred)
|
||||||
@@ -760,6 +763,8 @@ class DashboardQueries:
|
|||||||
if trial.get("available") and int(trial.get("total") or 0) > 0:
|
if trial.get("available") and int(trial.get("total") or 0) > 0:
|
||||||
return trial
|
return trial
|
||||||
growth_days = 1
|
growth_days = 1
|
||||||
|
else:
|
||||||
|
growth_days = 7
|
||||||
growth_days = int(growth_days)
|
growth_days = int(growth_days)
|
||||||
if growth_days < 1 or growth_days > 365:
|
if growth_days < 1 or growth_days > 365:
|
||||||
raise ValueError("growth_days 必须在 1~365 之间")
|
raise ValueError("growth_days 必须在 1~365 之间")
|
||||||
@@ -942,6 +947,8 @@ class DashboardQueries:
|
|||||||
"within_days": within_days,
|
"within_days": within_days,
|
||||||
"year": year,
|
"year": year,
|
||||||
"growth_days": growth_days,
|
"growth_days": growth_days,
|
||||||
|
# days:与 growth_days 同步;首页 auto 时为 1|7,供前端链接/展示
|
||||||
|
"days": growth_days,
|
||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"total": total,
|
"total": total,
|
||||||
|
|||||||
@@ -650,11 +650,10 @@ def _year_param(query: Mapping[str, list[str]]) -> int | None:
|
|||||||
|
|
||||||
|
|
||||||
def _growth_days_param(query: Mapping[str, list[str]]) -> int | str:
|
def _growth_days_param(query: Mapping[str, list[str]]) -> int | str:
|
||||||
"""榜单传 1~365;首页可传 auto(由查询层按 home_growth_days 解析)。"""
|
"""省略或 auto:查询层按 home_growth_days 解析(默认 7→1)。
|
||||||
|
榜单应显式传 1~365。"""
|
||||||
raw = _optional_str(query, "growth_days")
|
raw = _optional_str(query, "growth_days")
|
||||||
if raw is None:
|
if raw is None or raw == "auto":
|
||||||
return 7
|
|
||||||
if raw == "auto":
|
|
||||||
return "auto"
|
return "auto"
|
||||||
try:
|
try:
|
||||||
value = int(raw)
|
value = int(raw)
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export async function renderHome(root) {
|
|||||||
api("/api/overview"),
|
api("/api/overview"),
|
||||||
api("/api/videos", { sort: "published", within_days: 14, limit: 6 }),
|
api("/api/videos", { sort: "published", within_days: 14, limit: 6 }),
|
||||||
api("/api/videos", { sort: "heat", limit: 6 }),
|
api("/api/videos", { sort: "heat", limit: 6 }),
|
||||||
// auto:服务端按 analytics.home_growth_days 解析;默认先试 7 天再降到 1 天
|
// 不带天数:后端按 home_growth_days(默认 auto:有 7 日则 7,否则 1)决定,响应带 days
|
||||||
api("/api/videos", { sort: "growth", growth_days: "auto", limit: 6 }),
|
api("/api/videos", { sort: "growth", limit: 6 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fans = overview.fans;
|
const fans = overview.fans;
|
||||||
@@ -60,7 +60,8 @@ export async function renderHome(root) {
|
|||||||
const pending = overview.counts?.candidates_pending || 0;
|
const pending = overview.counts?.candidates_pending || 0;
|
||||||
// 候选提示仅对已登录(或未启用鉴权)用户可见,访客不展示运行期信息。
|
// 候选提示仅对已登录(或未启用鉴权)用户可见,访客不展示运行期信息。
|
||||||
const showCandidates = pending > 0 && canAccessProtected();
|
const showCandidates = pending > 0 && canAccessProtected();
|
||||||
const growthDays = Number(growth.growth_days) || 1;
|
// 以后端返回的 days(与 growth_days 同步)为准,避免前端再探查
|
||||||
|
const growthDays = Number(growth.days ?? growth.growth_days) || 1;
|
||||||
const growthLabel = `近 ${growthDays} 日`;
|
const growthLabel = `近 ${growthDays} 日`;
|
||||||
const growthEmptyTitle = growth.reason
|
const growthEmptyTitle = growth.reason
|
||||||
|| `近 ${growthDays} 日增长暂不可用`;
|
|| `近 ${growthDays} 日增长暂不可用`;
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ def test_growth_and_cold_start():
|
|||||||
try:
|
try:
|
||||||
growth = q.list_videos(sort="growth", growth_days=2)
|
growth = q.list_videos(sort="growth", growth_days=2)
|
||||||
assert growth["available"] is True
|
assert growth["available"] is True
|
||||||
|
assert growth["days"] == 2
|
||||||
|
assert growth["growth_days"] == 2
|
||||||
assert {item["aid"] for item in growth["items"]} == {101, 102}
|
assert {item["aid"] for item in growth["items"]} == {101, 102}
|
||||||
first = next(item for item in growth["items"] if item["aid"] == 101)
|
first = next(item for item in growth["items"] if item["aid"] == 101)
|
||||||
assert first["growth"]["available"] is True
|
assert first["growth"]["available"] is True
|
||||||
@@ -570,15 +572,19 @@ def test_growth_auto_falls_back_to_one_day():
|
|||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
db, q = setup(tmp)
|
db, q = setup(tmp)
|
||||||
try:
|
try:
|
||||||
auto = q.list_videos(sort="growth", growth_days="auto", page_size=6)
|
# 省略 growth_days 与显式 auto 等价:后端决定窗口并回传 days
|
||||||
|
auto = q.list_videos(sort="growth", page_size=6)
|
||||||
assert auto["available"] is True
|
assert auto["available"] is True
|
||||||
assert auto["growth_days"] == 1
|
assert auto["growth_days"] == 1
|
||||||
|
assert auto["days"] == 1
|
||||||
assert {item["aid"] for item in auto["items"]} == {101, 102}
|
assert {item["aid"] for item in auto["items"]} == {101, 102}
|
||||||
first = next(item for item in auto["items"] if item["aid"] == 101)
|
first = next(item for item in auto["items"] if item["aid"] == 101)
|
||||||
assert first["growth"]["delta"] == 100 # 相邻日
|
assert first["growth"]["delta"] == 100 # 相邻日
|
||||||
|
|
||||||
forced7 = q.list_videos(sort="growth", growth_days=7)
|
forced7 = q.list_videos(sort="growth", growth_days=7)
|
||||||
assert forced7["available"] is False
|
assert forced7["available"] is False
|
||||||
|
assert forced7["days"] == 7
|
||||||
|
assert forced7["growth_days"] == 7
|
||||||
assert "至少 8 天快照" in forced7["reason"]
|
assert "至少 8 天快照" in forced7["reason"]
|
||||||
|
|
||||||
# 配置钉死 7 天:auto 不再降级
|
# 配置钉死 7 天:auto 不再降级
|
||||||
@@ -589,8 +595,9 @@ def test_growth_auto_falls_back_to_one_day():
|
|||||||
target = max(path.stat().st_mtime_ns, time.time_ns()) + 1_000_000
|
target = max(path.stat().st_mtime_ns, time.time_ns()) + 1_000_000
|
||||||
os.utime(path, ns=(target, target))
|
os.utime(path, ns=(target, target))
|
||||||
DASHBOARD_CACHE.clear()
|
DASHBOARD_CACHE.clear()
|
||||||
pinned = q.list_videos(sort="growth", growth_days="auto")
|
pinned = q.list_videos(sort="growth") # omit → auto → 配置钉死 7
|
||||||
assert pinned["growth_days"] == 7
|
assert pinned["growth_days"] == 7
|
||||||
|
assert pinned["days"] == 7
|
||||||
assert pinned["available"] is False
|
assert pinned["available"] is False
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
@@ -108,8 +108,11 @@ def test_readonly_routes_and_static():
|
|||||||
|
|
||||||
status, _, payload = running.request("/api/videos")
|
status, _, payload = running.request("/api/videos")
|
||||||
assert status == 200
|
assert status == 200
|
||||||
items = json.loads(payload)["data"]["items"]
|
heat_payload = json.loads(payload)["data"]
|
||||||
|
items = heat_payload["items"]
|
||||||
assert items and items[0]["aid"] == 102
|
assert items and items[0]["aid"] == 102
|
||||||
|
# 非增长排序:省略 growth_days 时 days/growth_days 回落为 7,不做 7→1
|
||||||
|
assert heat_payload["days"] == heat_payload["growth_days"] == 7
|
||||||
|
|
||||||
status, _, payload = running.json("/api/videos?year=2026")
|
status, _, payload = running.json("/api/videos?year=2026")
|
||||||
assert status == 200
|
assert status == 200
|
||||||
@@ -120,6 +123,13 @@ def test_readonly_routes_and_static():
|
|||||||
assert status == 200
|
assert status == 200
|
||||||
assert payload["data"]["year"] == 2026
|
assert payload["data"]["year"] == 2026
|
||||||
|
|
||||||
|
# 首页形态:省略 growth_days → 后端 auto,响应带 days/growth_days
|
||||||
|
status, _, payload = running.json("/api/videos?sort=growth&limit=6")
|
||||||
|
assert status == 200
|
||||||
|
growth = payload["data"]
|
||||||
|
assert growth["days"] == growth["growth_days"] == 1
|
||||||
|
assert growth["available"] is True
|
||||||
|
|
||||||
status, _, payload = running.request("/api/ups")
|
status, _, payload = running.request("/api/ups")
|
||||||
assert status == 200
|
assert status == 200
|
||||||
assert json.loads(payload)["data"]["sort"] == "efficiency"
|
assert json.loads(payload)["data"]["sort"] == "efficiency"
|
||||||
|
|||||||
Reference in new issue
Block a user