diff --git a/WebUI/src/composables/useScheduler.js b/WebUI/src/composables/useScheduler.js index 5641ecb..c196f60 100644 --- a/WebUI/src/composables/useScheduler.js +++ b/WebUI/src/composables/useScheduler.js @@ -16,31 +16,41 @@ export function useScheduler() { } } - async function startScheduler(onSuccess) { + async function startScheduler(onSuccess, onError) { schedulerLoading.value = true try { const response = await schedulerService.start() if (response.code === 200) { - schedulerRunning.value = true - onSuccess?.('自动验证已启动') + schedulerRunning.value = response.data.running + if (response.data.running) { + onSuccess?.('自动验证已启动') + } else { + onError?.('启动调度器失败') + } } } catch (error) { console.error('启动调度器失败:', error) + onError?.('启动调度器失败') } finally { schedulerLoading.value = false } } - async function stopScheduler(onSuccess) { + async function stopScheduler(onSuccess, onError) { schedulerLoading.value = true try { const response = await schedulerService.stop() if (response.code === 200) { - schedulerRunning.value = false - onSuccess?.('自动验证已停止') + schedulerRunning.value = response.data.running + if (!response.data.running) { + onSuccess?.('自动验证已停止') + } else { + onError?.('停止调度器失败') + } } } catch (error) { console.error('停止调度器失败:', error) + onError?.('停止调度器失败') } finally { schedulerLoading.value = false } diff --git a/WebUI/src/views/ProxyList.vue b/WebUI/src/views/ProxyList.vue index 8e3107d..4d3ffbb 100644 --- a/WebUI/src/views/ProxyList.vue +++ b/WebUI/src/views/ProxyList.vue @@ -202,6 +202,10 @@ async function fetchProxies() { if (!success) { ElMessage.error('获取代理列表失败') + } else if (proxyStore.proxies.length === 0 && currentPage.value > 1) { + // 当前页无数据时自动回退一页 + currentPage.value -= 1 + return fetchProxies() } } catch (error) { if (error.name === 'AbortError') { diff --git a/WebUI/src/views/Settings.vue b/WebUI/src/views/Settings.vue index 2cef704..ea37b12 100644 --- a/WebUI/src/views/Settings.vue +++ b/WebUI/src/views/Settings.vue @@ -290,11 +290,17 @@ async function fetchSettings() { // ==================== 调度器控制 ==================== async function handleStartScheduler() { - await startScheduler((msg) => ElMessage.success(msg)) + await startScheduler( + (msg) => ElMessage.success(msg), + (msg) => ElMessage.error(msg) + ) } async function handleStopScheduler() { - await stopScheduler((msg) => ElMessage.success(msg)) + await stopScheduler( + (msg) => ElMessage.success(msg), + (msg) => ElMessage.error(msg) + ) } async function handleValidateNow() { diff --git a/app/api/lifespan.py b/app/api/lifespan.py index bbdbca7..5de7bb7 100644 --- a/app/api/lifespan.py +++ b/app/api/lifespan.py @@ -41,7 +41,7 @@ async def lifespan(app: FastAPI): connect_timeout=app_settings.validator_connect_timeout, 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"]) # 验证 WorkerPool diff --git a/app/api/routes/scheduler.py b/app/api/routes/scheduler.py index 6cef09b..c48112c 100644 --- a/app/api/routes/scheduler.py +++ b/app/api/routes/scheduler.py @@ -13,7 +13,9 @@ router = APIRouter(prefix="/api/scheduler", tags=["scheduler"]) async def _save_auto_validate_setting(enabled: bool, settings_repo: SettingsRepository): """保存自动验证设置""" 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") diff --git a/app/api/routes/settings.py b/app/api/routes/settings.py index 411c882..717304d 100644 --- a/app/api/routes/settings.py +++ b/app/api/routes/settings.py @@ -50,7 +50,7 @@ async def save_settings( validator._init_timeout = request.validation_timeout validator._init_connect_timeout = request.validation_timeout 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) # 先关闭现有 session,再重置 semaphore,避免竞态窗口 await validator.close() diff --git a/tests/integration/test_scheduler_api.py b/tests/integration/test_scheduler_api.py index 1ea9f80..a0d4989 100644 --- a/tests/integration/test_scheduler_api.py +++ b/tests/integration/test_scheduler_api.py @@ -92,6 +92,25 @@ class TestSchedulerAPI: job = executor.get_job(job_id) 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 async def test_scheduler_full_workflow(self, client): """测试调度器完整工作流""" diff --git a/tests/integration/test_settings_api.py b/tests/integration/test_settings_api.py index 12d9ced..ebf5561 100644 --- a/tests/integration/test_settings_api.py +++ b/tests/integration/test_settings_api.py @@ -164,3 +164,22 @@ class TestSettingsAPI: response = await client.get("/api/settings") saved_settings = response.json()["data"] 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"] == []