同步更新:重构路由、服务模块,更新前端构建

This commit is contained in:
2025-12-14 21:47:08 +08:00
parent e01a7b5235
commit a346509a5f
87 changed files with 9186 additions and 7826 deletions

7
tests/conftest.py Normal file
View File

@@ -0,0 +1,7 @@
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

View File

@@ -0,0 +1,56 @@
from __future__ import annotations
from datetime import datetime
from services.schedule_utils import compute_next_run_at, format_cst
from services.time_utils import BEIJING_TZ
def _dt(text: str) -> datetime:
naive = datetime.strptime(text, "%Y-%m-%d %H:%M:%S")
return BEIJING_TZ.localize(naive)
def test_compute_next_run_at_weekday_filter():
now = _dt("2025-01-06 07:00:00") # 周一
next_dt = compute_next_run_at(
now=now,
schedule_time="08:00",
weekdays="2", # 仅周二
random_delay=0,
last_run_at=None,
)
assert format_cst(next_dt) == "2025-01-07 08:00:00"
def test_compute_next_run_at_random_delay_within_window(monkeypatch):
now = _dt("2025-01-06 06:00:00")
# 固定随机值0 => window_startschedule_time-15min
monkeypatch.setattr("services.schedule_utils.random.randint", lambda a, b: 0)
next_dt = compute_next_run_at(
now=now,
schedule_time="08:00",
weekdays="1,2,3,4,5,6,7",
random_delay=1,
last_run_at=None,
)
assert format_cst(next_dt) == "2025-01-06 07:45:00"
def test_compute_next_run_at_skips_same_day_if_last_run_today(monkeypatch):
now = _dt("2025-01-06 06:00:00")
# 让次日的随机值固定,便于断言
monkeypatch.setattr("services.schedule_utils.random.randint", lambda a, b: 30)
next_dt = compute_next_run_at(
now=now,
schedule_time="08:00",
weekdays="1,2,3,4,5,6,7",
random_delay=1,
last_run_at="2025-01-06 01:00:00",
)
# 次日 window_start=07:45 + 30min => 08:15
assert format_cst(next_dt) == "2025-01-07 08:15:00"

77
tests/test_state.py Normal file
View File

@@ -0,0 +1,77 @@
import threading
import time
from services import state
def test_task_status_returns_copy():
account_id = "acc_test_copy"
state.safe_set_task_status(account_id, {"status": "运行中", "progress": {"items": 1}})
snapshot = state.safe_get_task_status(account_id)
snapshot["status"] = "已修改"
snapshot2 = state.safe_get_task_status(account_id)
assert snapshot2["status"] == "运行中"
def test_captcha_roundtrip():
session_id = "captcha_test"
state.safe_set_captcha(session_id, {"code": "1234", "expire_time": time.time() + 60, "failed_attempts": 0})
ok, msg = state.safe_verify_and_consume_captcha(session_id, "1234", max_attempts=5)
assert ok, msg
ok2, _ = state.safe_verify_and_consume_captcha(session_id, "1234", max_attempts=5)
assert not ok2
def test_ip_rate_limit_locking():
ip = "203.0.113.9"
ok, msg = state.check_ip_rate_limit(ip, max_attempts_per_hour=2, lock_duration_seconds=10)
assert ok and msg is None
locked = state.record_failed_captcha(ip, max_attempts_per_hour=2, lock_duration_seconds=10)
assert locked is False
locked2 = state.record_failed_captcha(ip, max_attempts_per_hour=2, lock_duration_seconds=10)
assert locked2 is True
ok3, msg3 = state.check_ip_rate_limit(ip, max_attempts_per_hour=2, lock_duration_seconds=10)
assert ok3 is False
assert "锁定" in (msg3 or "")
def test_batch_finalize_after_dispatch():
batch_id = "batch_test"
now_ts = time.time()
state.safe_create_batch(
batch_id,
{"screenshots": [], "total_accounts": 0, "completed": 0, "created_at": now_ts, "updated_at": now_ts},
)
state.safe_batch_append_result(batch_id, {"path": "a.png"})
state.safe_batch_append_result(batch_id, {"path": "b.png"})
batch_info = state.safe_finalize_batch_after_dispatch(batch_id, total_accounts=2, now_ts=time.time())
assert batch_info is not None
assert batch_info["completed"] == 2
def test_state_thread_safety_smoke():
errors = []
def worker(i: int):
try:
aid = f"acc_{i % 10}"
state.safe_set_task_status(aid, {"status": "运行中", "i": i})
_ = state.safe_get_task_status(aid)
except Exception as exc: # pragma: no cover
errors.append(exc)
threads = [threading.Thread(target=worker, args=(i,)) for i in range(200)]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors

View File

@@ -0,0 +1,146 @@
from __future__ import annotations
import threading
import time
from services.tasks import TaskScheduler
def test_task_scheduler_vip_priority(monkeypatch):
calls: list[str] = []
blocker_started = threading.Event()
blocker_release = threading.Event()
def fake_run_task(*, user_id, account_id, **kwargs):
calls.append(account_id)
if account_id == "block":
blocker_started.set()
blocker_release.wait(timeout=5)
import services.tasks as tasks_mod
monkeypatch.setattr(tasks_mod, "run_task", fake_run_task)
scheduler = TaskScheduler(max_global=1, max_per_user=1, max_queue_size=10)
try:
ok, _ = scheduler.submit_task(user_id=1, account_id="block", browse_type="应读", is_vip=False)
assert ok
assert blocker_started.wait(timeout=2)
ok2, _ = scheduler.submit_task(user_id=1, account_id="normal", browse_type="应读", is_vip=False)
ok3, _ = scheduler.submit_task(user_id=2, account_id="vip", browse_type="应读", is_vip=True)
assert ok2 and ok3
blocker_release.set()
deadline = time.time() + 3
while time.time() < deadline:
if calls[:3] == ["block", "vip", "normal"]:
break
time.sleep(0.05)
assert calls[:3] == ["block", "vip", "normal"]
finally:
scheduler.shutdown(timeout=2)
def test_task_scheduler_per_user_concurrency(monkeypatch):
started: list[str] = []
a1_started = threading.Event()
a1_release = threading.Event()
a2_started = threading.Event()
def fake_run_task(*, user_id, account_id, **kwargs):
started.append(account_id)
if account_id == "a1":
a1_started.set()
a1_release.wait(timeout=5)
if account_id == "a2":
a2_started.set()
import services.tasks as tasks_mod
monkeypatch.setattr(tasks_mod, "run_task", fake_run_task)
scheduler = TaskScheduler(max_global=2, max_per_user=1, max_queue_size=10)
try:
ok, _ = scheduler.submit_task(user_id=1, account_id="a1", browse_type="应读", is_vip=False)
assert ok
assert a1_started.wait(timeout=2)
ok2, _ = scheduler.submit_task(user_id=1, account_id="a2", browse_type="应读", is_vip=False)
assert ok2
# 同一用户并发=1a2 不应在 a1 未结束时启动
assert not a2_started.wait(timeout=0.3)
a1_release.set()
assert a2_started.wait(timeout=2)
assert started[0] == "a1"
assert "a2" in started
finally:
scheduler.shutdown(timeout=2)
def test_task_scheduler_cancel_pending(monkeypatch):
calls: list[str] = []
blocker_started = threading.Event()
blocker_release = threading.Event()
def fake_run_task(*, user_id, account_id, **kwargs):
calls.append(account_id)
if account_id == "block":
blocker_started.set()
blocker_release.wait(timeout=5)
import services.tasks as tasks_mod
monkeypatch.setattr(tasks_mod, "run_task", fake_run_task)
scheduler = TaskScheduler(max_global=1, max_per_user=1, max_queue_size=10)
try:
ok, _ = scheduler.submit_task(user_id=1, account_id="block", browse_type="应读", is_vip=False)
assert ok
assert blocker_started.wait(timeout=2)
ok2, _ = scheduler.submit_task(user_id=1, account_id="to_cancel", browse_type="应读", is_vip=False)
assert ok2
assert scheduler.cancel_pending_task(user_id=1, account_id="to_cancel") is True
blocker_release.set()
time.sleep(0.3)
assert "to_cancel" not in calls
finally:
scheduler.shutdown(timeout=2)
def test_task_scheduler_queue_full(monkeypatch):
blocker_started = threading.Event()
blocker_release = threading.Event()
def fake_run_task(*, user_id, account_id, **kwargs):
if account_id == "block":
blocker_started.set()
blocker_release.wait(timeout=5)
import services.tasks as tasks_mod
monkeypatch.setattr(tasks_mod, "run_task", fake_run_task)
scheduler = TaskScheduler(max_global=1, max_per_user=1, max_queue_size=1)
try:
ok, _ = scheduler.submit_task(user_id=1, account_id="block", browse_type="应读", is_vip=False)
assert ok
assert blocker_started.wait(timeout=2)
ok2, _ = scheduler.submit_task(user_id=1, account_id="p1", browse_type="应读", is_vip=False)
assert ok2
ok3, msg3 = scheduler.submit_task(user_id=1, account_id="p2", browse_type="应读", is_vip=False)
assert ok3 is False
assert "队列已满" in (msg3 or "")
finally:
blocker_release.set()
scheduler.shutdown(timeout=2)