Round 4 fixes: scheduler DB save check, empty validation_targets, proxy list page fallback, scheduler frontend state sync, tests

This commit is contained in:
祀梦
2026-04-05 10:31:20 +08:00
parent dc5f050683
commit d5fdfd65d9
8 changed files with 71 additions and 11 deletions

View File

@@ -16,31 +16,41 @@ export function useScheduler() {
} }
} }
async function startScheduler(onSuccess) { async function startScheduler(onSuccess, onError) {
schedulerLoading.value = true schedulerLoading.value = true
try { try {
const response = await schedulerService.start() const response = await schedulerService.start()
if (response.code === 200) { if (response.code === 200) {
schedulerRunning.value = true schedulerRunning.value = response.data.running
if (response.data.running) {
onSuccess?.('自动验证已启动') onSuccess?.('自动验证已启动')
} else {
onError?.('启动调度器失败')
}
} }
} catch (error) { } catch (error) {
console.error('启动调度器失败:', error) console.error('启动调度器失败:', error)
onError?.('启动调度器失败')
} finally { } finally {
schedulerLoading.value = false schedulerLoading.value = false
} }
} }
async function stopScheduler(onSuccess) { async function stopScheduler(onSuccess, onError) {
schedulerLoading.value = true schedulerLoading.value = true
try { try {
const response = await schedulerService.stop() const response = await schedulerService.stop()
if (response.code === 200) { if (response.code === 200) {
schedulerRunning.value = false schedulerRunning.value = response.data.running
if (!response.data.running) {
onSuccess?.('自动验证已停止') onSuccess?.('自动验证已停止')
} else {
onError?.('停止调度器失败')
}
} }
} catch (error) { } catch (error) {
console.error('停止调度器失败:', error) console.error('停止调度器失败:', error)
onError?.('停止调度器失败')
} finally { } finally {
schedulerLoading.value = false schedulerLoading.value = false
} }

View File

@@ -202,6 +202,10 @@ async function fetchProxies() {
if (!success) { if (!success) {
ElMessage.error('获取代理列表失败') ElMessage.error('获取代理列表失败')
} else if (proxyStore.proxies.length === 0 && currentPage.value > 1) {
// 当前页无数据时自动回退一页
currentPage.value -= 1
return fetchProxies()
} }
} catch (error) { } catch (error) {
if (error.name === 'AbortError') { if (error.name === 'AbortError') {

View File

@@ -290,11 +290,17 @@ async function fetchSettings() {
// ==================== 调度器控制 ==================== // ==================== 调度器控制 ====================
async function handleStartScheduler() { async function handleStartScheduler() {
await startScheduler((msg) => ElMessage.success(msg)) await startScheduler(
(msg) => ElMessage.success(msg),
(msg) => ElMessage.error(msg)
)
} }
async function handleStopScheduler() { async function handleStopScheduler() {
await stopScheduler((msg) => ElMessage.success(msg)) await stopScheduler(
(msg) => ElMessage.success(msg),
(msg) => ElMessage.error(msg)
)
} }
async function handleValidateNow() { async function handleValidateNow() {

View File

@@ -41,7 +41,7 @@ async def lifespan(app: FastAPI):
connect_timeout=app_settings.validator_connect_timeout, connect_timeout=app_settings.validator_connect_timeout,
max_concurrency=db_settings.get("default_concurrency", app_settings.validator_max_concurrency), max_concurrency=db_settings.get("default_concurrency", app_settings.validator_max_concurrency),
) )
if db_settings.get("validation_targets"): if db_settings.get("validation_targets") is not None:
validator.update_test_urls(db_settings["validation_targets"]) validator.update_test_urls(db_settings["validation_targets"])
# 验证 WorkerPool # 验证 WorkerPool

View File

@@ -13,7 +13,9 @@ router = APIRouter(prefix="/api/scheduler", tags=["scheduler"])
async def _save_auto_validate_setting(enabled: bool, settings_repo: SettingsRepository): async def _save_auto_validate_setting(enabled: bool, settings_repo: SettingsRepository):
"""保存自动验证设置""" """保存自动验证设置"""
async with get_db() as db: async with get_db() as db:
await settings_repo.save(db, {"auto_validate": enabled}) success = await settings_repo.save(db, {"auto_validate": enabled})
if not success:
raise RuntimeError("保存自动验证设置失败")
@router.post("/start") @router.post("/start")

View File

@@ -50,7 +50,7 @@ async def save_settings(
validator._init_timeout = request.validation_timeout validator._init_timeout = request.validation_timeout
validator._init_connect_timeout = request.validation_timeout validator._init_connect_timeout = request.validation_timeout
validator._init_max_concurrency = request.default_concurrency validator._init_max_concurrency = request.default_concurrency
if request.validation_targets: if request.validation_targets is not None:
validator.update_test_urls(request.validation_targets) validator.update_test_urls(request.validation_targets)
# 先关闭现有 session再重置 semaphore避免竞态窗口 # 先关闭现有 session再重置 semaphore避免竞态窗口
await validator.close() await validator.close()

View File

@@ -92,6 +92,25 @@ class TestSchedulerAPI:
job = executor.get_job(job_id) job = executor.get_job(job_id)
assert job is not None assert job is not None
@pytest.mark.asyncio
async def test_start_scheduler_db_save_failure(self, client, monkeypatch):
"""测试启动调度器时数据库保存失败应返回 running=False"""
from app.repositories.settings_repo import SettingsRepository
# lifespan 启动时调度器可能已自动启动,先停止它
await client.post("/api/scheduler/stop")
async def mock_save(*args, **kwargs):
return False
monkeypatch.setattr(SettingsRepository, "save", mock_save)
response = await client.post("/api/scheduler/start")
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
assert data["data"]["running"] is False
assert "失败" in data["message"]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_scheduler_full_workflow(self, client): async def test_scheduler_full_workflow(self, client):
"""测试调度器完整工作流""" """测试调度器完整工作流"""

View File

@@ -164,3 +164,22 @@ class TestSettingsAPI:
response = await client.get("/api/settings") response = await client.get("/api/settings")
saved_settings = response.json()["data"] saved_settings = response.json()["data"]
assert saved_settings["validation_targets"] == test_settings["validation_targets"] assert saved_settings["validation_targets"] == test_settings["validation_targets"]
@pytest.mark.asyncio
async def test_save_settings_empty_validation_targets(self, client):
"""测试保存空的 validation_targets"""
response = await client.get("/api/settings")
current_settings = response.json()["data"]
new_settings = current_settings.copy()
new_settings["validation_targets"] = []
response = await client.post("/api/settings", json=new_settings)
assert response.status_code == 200
data = response.json()
assert data["data"]["validation_targets"] == []
# 读取确认
response = await client.get("/api/settings")
saved_settings = response.json()["data"]
assert saved_settings["validation_targets"] == []