feat: home growth auto(7→1) and hourly recent-video snapshot

- Add analytics.home_growth_days (auto|1|7); API growth_days=auto tries 7 then 1
- Homepage shows active window and growth-board link; clearer empty reason
- New snapshot_recent scheduler task (schedule.recent_snapshot_minutes/days)
- run_recent_videos refreshes new aids only; never touches completed/backup
- Full 00:01 job keeps ok=1 skip (covers hourly refresh within 1h); mutex with recent
This commit is contained in:
bot-ci committed 2026-11-02 10:00:02 +08:00
1 parent 04716aec1b
commit 9163f47b76
15 files changed
+496 -22

No files matched your search

+96
View File
@@ -436,6 +436,100 @@ def test_backup_failure_retries_without_api():
db.close()
def test_run_recent_videos_refreshes_without_completion_markers():
"""小时任务只刷新近窗新片,不写完成日 / 备份。"""
clock = 1_800_000_000
with tempfile.TemporaryDirectory() as tmp:
transport = SnapshotTransport({
1: view_for(1, view=111),
2: view_for(2, view=222),
3: view_for(3, view=333),
})
db, repo, service = make_service(tmp, transport, track_fans=())
try:
# 1/2 在墙钟近 2 天内;3 为旧片
add_video(repo, 1)
add_video(repo, 2)
add_video(repo, 3)
repo.db.write(lambda conn: conn.executemany(
"UPDATE video SET pubdate = ? WHERE aid = ?",
[
(clock - 3600, 1),
(clock - 86400, 2),
(clock - 10 * 86400, 3),
],
))
# 预置同日 ok=1,验证小时任务可覆盖刷新
repo.upsert_snapshot(
DATE, 1,
metrics={"view": 1, "like": 0, "coin": 0, "favorite": 0,
"share": 0, "reply": 0, "danmaku": 0},
ok=True, err=None, captured_at=clock - 100,
)
assert repo.kv_get(service.completed_key(DATE)) is None
summary = service.run_recent_videos(date=DATE, recent_days=2)
assert summary.total == 2
assert summary.success == 2
assert summary.failed == 0
assert set(repo.video_aids_recent_for_snapshot(
since_pubdate=clock - 2 * 86400
)) == {1, 2}
row = repo.db.read(lambda conn: dict(conn.execute(
"SELECT view, ok, captured_at FROM snapshot WHERE stat_date=? AND aid=1",
(DATE,),
).fetchone()))
assert row["view"] == 111
assert row["ok"] == 1
assert row["captured_at"] == clock
# 完成日 / 备份标记保持原样
assert repo.kv_get(service.completed_key(DATE)) is None
assert repo.kv_get("snapshot:last_completed") is None
assert repo.kv_get(service.backup_key(DATE)) is None
assert transport.count("/web-interface/view") == 2
# 全量任务随后才写完成日;且已 ok=1 的跳过
full = service.run_videos(date=DATE)
assert full.skipped >= 2 # 1、2 已被小时任务写过
assert full.success >= 1 # 至少旧片 3
assert repo.kv_get(service.completed_key(DATE)) == "1"
assert repo.kv_get("snapshot:last_completed") == DATE
finally:
db.close()
def test_full_snapshot_skips_ok_rows_including_hourly_refresh():
"""00:01 全量:同日 ok=1(含小时任务刚刷)直接跳过。"""
clock = 1_800_000_000
with tempfile.TemporaryDirectory() as tmp:
transport = SnapshotTransport({1: view_for(1), 2: view_for(2)})
db, repo, service = make_service(tmp, transport, track_fans=())
try:
add_video(repo, 1)
add_video(repo, 2)
repo.upsert_snapshot(
DATE, 1,
metrics={"view": 9, "like": 0, "coin": 0, "favorite": 0,
"share": 0, "reply": 0, "danmaku": 0},
ok=True, err=None, captured_at=clock - 30, # 「1 小时内」
)
result = service.run_videos(date=DATE)
assert result.skipped == 1
assert result.success == 1
# aid=1 保持小时任务写入的 view=9,未被全量覆盖
row = repo.db.read(lambda conn: dict(conn.execute(
"SELECT view FROM snapshot WHERE stat_date=? AND aid=1",
(DATE,),
).fetchone()))
assert row["view"] == 9
assert transport.count("/web-interface/view") == 1
finally:
db.close()
def main():
test_snapshot_success_resume_and_force()
test_resume_skips_ok_and_retries_failed()
@@ -449,6 +543,8 @@ def main():
test_fan_resume_preserves_successful_value()
test_force_interrupt_leaves_incomplete_marker()
test_backup_failure_retries_without_api()
test_run_recent_videos_refreshes_without_completion_markers()
test_full_snapshot_skips_ok_rows_including_hourly_refresh()
print("OK: 每日快照、断点续跑、失败占位与粉丝采集测试全部通过")