后端重构: - 新增分层架构:API Routes -> Services -> Repositories -> Infrastructure - 彻底移除全局单例,全面采用 FastAPI 依赖注入 - 新增 api/ 目录拆分路由(proxies, plugins, scheduler, settings, stats) - 新增 services/ 业务逻辑层:ProxyService, PluginService, SchedulerService, ValidatorService, SettingsService - 新增 repositories/ 数据访问层:ProxyRepository, SettingsRepository, PluginSettingsRepository - 新增 models/ 层:Pydantic Schemas + Domain Models - 重写 core/config.py:采用 Pydantic Settings 管理配置 - 新增 core/db.py:基于 asynccontextmanager 的连接管理,支持数据库迁移 - 新增 core/exceptions.py:统一业务异常体系 插件系统重构(核心): - 新增 core/plugin_system/:BaseCrawlerPlugin + PluginRegistry - 采用显式注册模式(装饰器 + plugins/__init__.py),类型安全、测试友好 - 新增 plugins/base.py:BaseHTTPPlugin 通用 HTTP 爬虫基类 - 迁移全部 7 个插件到新架构(fate0, proxylist_download, ip3366, ip89, kuaidaili, speedx, yundaili) - 插件状态持久化到 plugin_settings 表 任务调度重构: - 新增 core/tasks/queue.py:ValidationQueue + WorkerPool - 解耦爬取与验证:爬虫只负责爬取,代理提交队列后由 Worker 异步验证 - 调度器定时从数据库拉取存量代理并分批投入验证队列 前端调整: - 新增 frontend/src/services/ 层拆分 API 调用逻辑 - 调整 stores/ 和 views/ 使用 Service 层 - 保持 API 兼容性,页面无需大幅修改 其他: - 新增 main.py 作为新入口 - 新增 DESIGN.md 架构设计文档 - 更新 requirements.txt 增加 pydantic-settings
471 lines
12 KiB
Vue
471 lines
12 KiB
Vue
<template>
|
|
<div class="page-container">
|
|
<PageHeader title="系统设置" :icon="Setting" />
|
|
|
|
<!-- 验证调度器控制 -->
|
|
<el-card class="settings-card scheduler-card" shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span class="card-title">
|
|
<el-icon class="header-icon"><Timer /></el-icon>
|
|
验证调度器
|
|
</span>
|
|
<div class="scheduler-status">
|
|
<span class="status-dot" :class="{ active: schedulerRunning }"></span>
|
|
<span class="status-text">{{ schedulerRunning ? '运行中' : '已停止' }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="scheduler-actions">
|
|
<el-button
|
|
type="success"
|
|
@click="handleStartScheduler"
|
|
:disabled="schedulerRunning"
|
|
:loading="schedulerLoading"
|
|
>
|
|
<el-icon class="btn-icon"><VideoPlay /></el-icon>
|
|
启动自动验证
|
|
</el-button>
|
|
|
|
<el-button
|
|
type="danger"
|
|
@click="handleStopScheduler"
|
|
:disabled="!schedulerRunning"
|
|
:loading="schedulerLoading"
|
|
>
|
|
<el-icon class="btn-icon"><VideoPause /></el-icon>
|
|
停止自动验证
|
|
</el-button>
|
|
|
|
<el-button
|
|
type="primary"
|
|
@click="handleValidateNow"
|
|
:loading="validating"
|
|
>
|
|
<el-icon class="btn-icon"><Refresh /></el-icon>
|
|
立即验证全部
|
|
</el-button>
|
|
</div>
|
|
|
|
<div class="scheduler-info">
|
|
<el-alert
|
|
:title="schedulerInfo"
|
|
type="info"
|
|
:closable="false"
|
|
show-icon
|
|
/>
|
|
</div>
|
|
</el-card>
|
|
|
|
<!-- 基础配置 -->
|
|
<el-card class="settings-card" shadow="hover" v-loading="loading">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span class="card-title">
|
|
<el-icon class="header-icon"><Tools /></el-icon>
|
|
基础配置
|
|
</span>
|
|
<el-button
|
|
type="primary"
|
|
@click="handleSave"
|
|
size="large"
|
|
:loading="saving"
|
|
>
|
|
<el-icon class="btn-icon"><DocumentChecked /></el-icon>
|
|
保存配置
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
|
|
<el-form
|
|
:model="settings"
|
|
label-width="180px"
|
|
class="settings-form"
|
|
:rules="formRules"
|
|
ref="formRef"
|
|
>
|
|
<el-divider content-position="left">爬虫配置</el-divider>
|
|
|
|
<el-form-item label="爬取超时" prop="crawl_timeout">
|
|
<el-input-number
|
|
v-model="settings.crawl_timeout"
|
|
:min="5"
|
|
:max="120"
|
|
:step="5"
|
|
class="setting-input"
|
|
/>
|
|
<span class="setting-suffix">秒</span>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="最大重试次数" prop="max_retries">
|
|
<el-input-number
|
|
v-model="settings.max_retries"
|
|
:min="0"
|
|
:max="10"
|
|
class="setting-input"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-divider content-position="left">验证配置</el-divider>
|
|
|
|
<el-form-item label="验证超时" prop="validation_timeout">
|
|
<el-input-number
|
|
v-model="settings.validation_timeout"
|
|
:min="3"
|
|
:max="60"
|
|
:step="1"
|
|
class="setting-input"
|
|
/>
|
|
<span class="setting-suffix">秒</span>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="验证并发数" prop="default_concurrency">
|
|
<el-input-number
|
|
v-model="settings.default_concurrency"
|
|
:min="10"
|
|
:max="200"
|
|
:step="10"
|
|
class="setting-input"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="自动验证间隔" prop="validate_interval_minutes">
|
|
<el-input-number
|
|
v-model="settings.validate_interval_minutes"
|
|
:min="5"
|
|
:max="1440"
|
|
:step="5"
|
|
class="setting-input"
|
|
/>
|
|
<span class="setting-suffix">分钟</span>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="启用自动验证" prop="auto_validate">
|
|
<el-switch
|
|
v-model="settings.auto_validate"
|
|
active-text="开启"
|
|
inactive-text="关闭"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-divider content-position="left">代理评分配置</el-divider>
|
|
|
|
<el-form-item label="最低代理分数" prop="min_proxy_score">
|
|
<el-input-number
|
|
v-model="settings.min_proxy_score"
|
|
:min="0"
|
|
:max="100"
|
|
:step="1"
|
|
class="setting-input"
|
|
/>
|
|
<span class="setting-hint">分数低于此值的代理将被隐藏</span>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="代理过期时间" prop="proxy_expiry_days">
|
|
<el-input-number
|
|
v-model="settings.proxy_expiry_days"
|
|
:min="1"
|
|
:max="30"
|
|
:step="1"
|
|
class="setting-input"
|
|
/>
|
|
<span class="setting-suffix">天</span>
|
|
<span class="setting-hint">超过此时间未验证的代理将被清理</span>
|
|
</el-form-item>
|
|
</el-form>
|
|
</el-card>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted, computed } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import {
|
|
Setting,
|
|
DocumentChecked,
|
|
Tools,
|
|
Timer,
|
|
VideoPlay,
|
|
VideoPause,
|
|
Refresh
|
|
} from '@element-plus/icons-vue'
|
|
import { settingService } from '../services/settingService'
|
|
import { schedulerService } from '../services/schedulerService'
|
|
import PageHeader from '../components/PageHeader.vue'
|
|
|
|
// ==================== 状态 ====================
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const formRef = ref(null)
|
|
|
|
const schedulerRunning = ref(false)
|
|
const schedulerLoading = ref(false)
|
|
const validating = ref(false)
|
|
|
|
const settings = reactive({
|
|
crawl_timeout: 30,
|
|
validation_timeout: 10,
|
|
max_retries: 3,
|
|
default_concurrency: 50,
|
|
min_proxy_score: 0,
|
|
proxy_expiry_days: 7,
|
|
auto_validate: true,
|
|
validate_interval_minutes: 30
|
|
})
|
|
|
|
// ==================== 计算属性 ====================
|
|
const schedulerInfo = computed(() => {
|
|
if (schedulerRunning.value) {
|
|
return `验证调度器正在运行,每 ${settings.validate_interval_minutes} 分钟自动验证一次所有代理`
|
|
} else {
|
|
return '验证调度器已停止,代理不会自动验证,建议定期手动验证或开启自动验证'
|
|
}
|
|
})
|
|
|
|
// ==================== 表单验证规则 ====================
|
|
const formRules = {
|
|
crawl_timeout: [{ type: 'number', min: 5, max: 120, message: '范围 5-120 秒', trigger: 'blur' }],
|
|
validation_timeout: [{ type: 'number', min: 3, max: 60, message: '范围 3-60 秒', trigger: 'blur' }],
|
|
max_retries: [{ type: 'number', min: 0, max: 10, message: '范围 0-10', trigger: 'blur' }],
|
|
default_concurrency: [{ type: 'number', min: 10, max: 200, message: '范围 10-200', trigger: 'blur' }],
|
|
validate_interval_minutes: [{ type: 'number', min: 5, max: 1440, message: '范围 5-1440 分钟', trigger: 'blur' }],
|
|
min_proxy_score: [{ type: 'number', min: 0, max: 100, message: '范围 0-100', trigger: 'blur' }],
|
|
proxy_expiry_days: [{ type: 'number', min: 1, max: 30, message: '范围 1-30 天', trigger: 'blur' }]
|
|
}
|
|
|
|
// ==================== 数据获取 ====================
|
|
async function fetchSettings() {
|
|
loading.value = true
|
|
try {
|
|
const response = await settingService.getSettings()
|
|
if (response.code === 200) {
|
|
Object.assign(settings, response.data)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取设置失败:', error)
|
|
ElMessage.error('获取设置失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function fetchSchedulerStatus() {
|
|
try {
|
|
const response = await schedulerService.getStatus()
|
|
if (response.code === 200) {
|
|
schedulerRunning.value = response.data.running
|
|
}
|
|
} catch (error) {
|
|
console.error('获取调度器状态失败:', error)
|
|
}
|
|
}
|
|
|
|
// ==================== 调度器控制 ====================
|
|
async function handleStartScheduler() {
|
|
schedulerLoading.value = true
|
|
try {
|
|
const response = await schedulerService.start()
|
|
if (response.code === 200) {
|
|
schedulerRunning.value = true
|
|
ElMessage.success('自动验证已启动')
|
|
} else {
|
|
ElMessage.error('启动失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('启动调度器失败:', error)
|
|
ElMessage.error('启动失败')
|
|
} finally {
|
|
schedulerLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function handleStopScheduler() {
|
|
schedulerLoading.value = true
|
|
try {
|
|
const response = await schedulerService.stop()
|
|
if (response.code === 200) {
|
|
schedulerRunning.value = false
|
|
ElMessage.success('自动验证已停止')
|
|
} else {
|
|
ElMessage.error('停止失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('停止调度器失败:', error)
|
|
ElMessage.error('停止失败')
|
|
} finally {
|
|
schedulerLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function handleValidateNow() {
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
'确定要立即验证所有代理吗?这可能需要一些时间。',
|
|
'确认验证',
|
|
{
|
|
confirmButtonText: '开始验证',
|
|
cancelButtonText: '取消',
|
|
type: 'info'
|
|
}
|
|
)
|
|
|
|
validating.value = true
|
|
const response = await schedulerService.validateNow()
|
|
if (response.code === 200) {
|
|
ElMessage.success('全量验证已启动,请在日志中查看进度')
|
|
} else {
|
|
ElMessage.error('启动验证失败')
|
|
}
|
|
} catch (error) {
|
|
if (error !== 'cancel') {
|
|
console.error('启动验证失败:', error)
|
|
ElMessage.error('启动验证失败')
|
|
}
|
|
} finally {
|
|
validating.value = false
|
|
}
|
|
}
|
|
|
|
// ==================== 保存 ====================
|
|
async function handleSave() {
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
if (!valid) return
|
|
|
|
saving.value = true
|
|
try {
|
|
const response = await settingService.saveSettings(settings)
|
|
|
|
if (response.code === 200) {
|
|
ElMessage.success('配置保存成功')
|
|
// 刷新调度器状态
|
|
await fetchSchedulerStatus()
|
|
} else {
|
|
ElMessage.error('配置保存失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('保存设置失败:', error)
|
|
ElMessage.error('配置保存失败')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
// ==================== 生命周期 ====================
|
|
onMounted(() => {
|
|
fetchSettings()
|
|
fetchSchedulerStatus()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.settings-card {
|
|
border-radius: var(--radius-lg);
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.settings-card:hover {
|
|
border-color: var(--border-light);
|
|
}
|
|
|
|
.scheduler-card {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.header-icon {
|
|
margin-right: 8px;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.btn-icon {
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.scheduler-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: var(--text-muted);
|
|
transition: background 0.3s;
|
|
}
|
|
|
|
.status-dot.active {
|
|
background: #67c23a;
|
|
box-shadow: 0 0 8px #67c23a;
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.scheduler-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.scheduler-info {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.settings-form {
|
|
padding: 16px;
|
|
max-width: 800px;
|
|
}
|
|
|
|
.setting-input {
|
|
width: 300px;
|
|
}
|
|
|
|
.setting-suffix {
|
|
margin-left: 10px;
|
|
color: var(--text-muted);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.setting-hint {
|
|
margin-left: 10px;
|
|
color: var(--text-muted);
|
|
font-size: 13px;
|
|
}
|
|
|
|
:deep(.el-form-item__label) {
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
:deep(.el-divider__text) {
|
|
background: var(--surface);
|
|
color: var(--primary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
:deep(.el-alert) {
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
}
|
|
</style>
|