任务管理页面后端优化:提升进度更新频率和状态详细程度

1. 提高爬取阶段进度更新频率:从每10个改为每5个代理更新一次
2. 提高验证阶段进度更新频率:从每5个改为每验证1个代理就更新一次
3. 添加进度百分比计算所需字段:在progress消息中添加current和total字段
4. 增强状态信息详细程度:
   - 添加connecting状态:正在连接插件源
   - 添加starting状态:正在启动爬虫
   - 添加crawling_start状态:开始爬取代理
   - 添加validating_start状态:开始验证代理
   - 在进度消息中添加message字段,显示更详细的进度描述

这些改进可以让前端显示更实时、更详细的任务进度和状态信息
This commit is contained in:
祀梦
2026-01-27 23:15:43 +08:00
parent 466c77b28d
commit b5932a95b2
6 changed files with 844 additions and 51 deletions

View File

@@ -6,6 +6,19 @@ const api = axios.create({
timeout: 30000 timeout: 30000
}) })
api.interceptors.request.use(
config => {
const apiKey = localStorage.getItem('api_key')
if (apiKey) {
config.headers['X-API-Key'] = apiKey
}
return config
},
error => {
return Promise.reject(error)
}
)
api.interceptors.response.use( api.interceptors.response.use(
response => response.data, response => response.data,
error => { error => {
@@ -20,14 +33,22 @@ export const statsAPI = {
} }
export const proxiesAPI = { export const proxiesAPI = {
getProxies: (params) => api.post('/api/proxies', params), getProxies: (params) => {
const cleanedParams = {}
Object.keys(params).forEach(key => {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
cleanedParams[key] = params[key]
}
})
return api.post('/api/proxies', cleanedParams)
},
getRandomProxy: () => api.get('/api/proxies/random'), getRandomProxy: () => api.get('/api/proxies/random'),
getProxyDetail: (ip, port) => api.get(`/api/proxies/${ip}/${port}`), getProxyDetail: (ip, port) => api.get(`/api/proxies/${ip}/${port}`),
deleteProxy: (ip, port) => api.delete(`/api/proxies/${ip}/${port}`), deleteProxy: (ip, port) => api.delete(`/api/proxies/${ip}/${port}`),
batchDeleteProxies: (proxies) => api.post('/api/proxies/batch-delete', { proxies }), batchDeleteProxies: (proxies) => api.post('/api/proxies/batch-delete', { proxies }),
cleanInvalidProxies: () => api.delete('/api/proxies/clean-invalid'), cleanInvalidProxies: () => api.delete('/api/proxies/clean-invalid'),
exportProxies: (format, protocol) => api.get(`/api/proxies/export/${format}`, { exportProxies: (format, protocol) => api.get(`/api/proxies/export/${format}`, {
params: { protocol }, params: protocol ? { protocol } : {},
responseType: 'blob' responseType: 'blob'
}) })
} }

View File

@@ -1,6 +1,7 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import router from './router' import router from './router'
import './style.css' import './style.css'
@@ -12,6 +13,8 @@ const pinia = createPinia()
app.use(pinia) app.use(pinia)
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus, {
locale: zhCn,
})
app.mount('#app') app.mount('#app')

View File

@@ -5,7 +5,7 @@
<el-card class="filter-card" shadow="hover"> <el-card class="filter-card" shadow="hover">
<el-form :inline="true" :model="filterForm" class="form-row"> <el-form :inline="true" :model="filterForm" class="form-row">
<el-form-item label="协议类型"> <el-form-item label="协议类型">
<el-select v-model="filterForm.protocol" placeholder="全部" clearable style="width: 120px"> <el-select v-model="filterForm.protocol" placeholder="全部" clearable style="width: 120px" @change="handleSearch">
<el-option label="全部" value=""></el-option> <el-option label="全部" value=""></el-option>
<el-option label="HTTP" value="http"></el-option> <el-option label="HTTP" value="http"></el-option>
<el-option label="HTTPS" value="https"></el-option> <el-option label="HTTPS" value="https"></el-option>
@@ -14,24 +14,14 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="最低分数"> <el-form-item label="最低分数">
<el-input-number v-model="filterForm.minScore" :min="0" :max="10" style="width: 120px" /> <el-input-number v-model="filterForm.minScore" :min="0" :max="10" style="width: 120px" @change="handleSearch" />
</el-form-item> </el-form-item>
<el-form-item label="排序方式"> <el-form-item label="排序方式">
<el-select v-model="filterForm.sortBy" style="width: 140px"> <el-select v-model="filterForm.sortBy" style="width: 140px" @change="handleSearch">
<el-option label="更新时间" value="last_check"></el-option> <el-option label="更新时间" value="last_check"></el-option>
<el-option label="分数" value="score"></el-option> <el-option label="分数" value="score"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<span class="btn-icon">🔍</span>
搜索
</el-button>
<el-button @click="handleReset">
<span class="btn-icon">🔄</span>
重置
</el-button>
</el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@@ -40,11 +30,15 @@
<div class="card-header"> <div class="card-header">
<span class="card-title">代理详情</span> <span class="card-title">代理详情</span>
<div class="header-actions"> <div class="header-actions">
<el-button type="danger" size="small" @click="handleBatchDelete" :disabled="selectedProxies.length === 0"> <el-button-group>
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedProxies.length === 0">
批量删除 批量删除
</el-button> </el-button>
<el-dropdown @command="handleExport" split-button type="success"> <el-dropdown trigger="click" @command="handleExport">
<el-button type="success">
导出 导出
<el-icon class="el-icon--right"><component :is="ArrowDownIcon" /></el-icon>
</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="txt">TXT格式</el-dropdown-item> <el-dropdown-item command="txt">TXT格式</el-dropdown-item>
@@ -53,6 +47,7 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</el-button-group>
</div> </div>
</div> </div>
</template> </template>
@@ -76,15 +71,14 @@
</el-table-column> </el-table-column>
<el-table-column prop="score" label="分数" width="100"> <el-table-column prop="score" label="分数" width="100">
<template #default="scope"> <template #default="scope">
<el-rate <span class="score-value">{{ scope.row.score || 0 }}</span>
:model-value="scope.row.score || 0" </template>
disabled </el-table-column>
show-score <el-table-column prop="last_check" label="最后检查时间">
:score-template="scope.row.score ? '{value}' : '0'" <template #default="scope">
/> {{ formatDateTime(scope.row.last_check) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="last_check" label="最后检查时间" />
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="200" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button <el-button
@@ -123,9 +117,12 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowDown } from '@element-plus/icons-vue'
import { useProxyStore } from '../stores/proxy' import { useProxyStore } from '../stores/proxy'
import PageHeader from '../components/PageHeader.vue' import PageHeader from '../components/PageHeader.vue'
const ArrowDownIcon = ArrowDown
const proxyStore = useProxyStore() const proxyStore = useProxyStore()
const currentPage = ref(1) const currentPage = ref(1)
@@ -149,11 +146,23 @@ function getProtocolType(protocol) {
return types[protocol] || 'info' return types[protocol] || 'info'
} }
function formatDateTime(dateTimeStr) {
if (!dateTimeStr) return '-'
const date = new Date(dateTimeStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
async function fetchProxies() { async function fetchProxies() {
await proxyStore.fetchProxies({ await proxyStore.fetchProxies({
page: currentPage.value, page: currentPage.value,
page_size: pageSize.value, page_size: pageSize.value,
protocol: filterForm.protocol || undefined, protocol: filterForm.protocol || null,
min_score: filterForm.minScore, min_score: filterForm.minScore,
sort_by: filterForm.sortBy, sort_by: filterForm.sortBy,
sort_order: filterForm.sortOrder sort_order: filterForm.sortOrder
@@ -223,7 +232,7 @@ async function handleBatchDelete() {
} }
async function handleExport(format) { async function handleExport(format) {
const success = await proxyStore.exportProxies(format, filterForm.protocol || undefined) const success = await proxyStore.exportProxies(format, filterForm.protocol || null)
if (success) { if (success) {
ElMessage.success(`导出 ${format.toUpperCase()} 格式成功啦~`) ElMessage.success(`导出 ${format.toUpperCase()} 格式成功啦~`)
} }

View File

@@ -14,6 +14,17 @@
</template> </template>
<el-form :model="settings" label-width="150px" class="settings-form"> <el-form :model="settings" label-width="150px" class="settings-form">
<el-form-item label="管理员API Key">
<el-input
v-model="settings.api_key"
placeholder="请输入管理员API Key"
type="password"
show-password
class="setting-input"
/>
<div class="setting-hint">用于执行管理操作的API Key</div>
</el-form-item>
<el-form-item label="数据库路径"> <el-form-item label="数据库路径">
<el-input v-model="settings.db_path" placeholder="数据库文件路径" /> <el-input v-model="settings.db_path" placeholder="数据库文件路径" />
</el-form-item> </el-form-item>
@@ -92,6 +103,7 @@ import PageHeader from '../components/PageHeader.vue'
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
const settings = reactive({ const settings = reactive({
api_key: '',
db_path: '', db_path: '',
crawl_timeout: 30, crawl_timeout: 30,
validation_timeout: 10, validation_timeout: 10,
@@ -109,6 +121,7 @@ async function fetchSettings() {
const data = await response.json() const data = await response.json()
Object.assign(settings, data) Object.assign(settings, data)
} }
settings.api_key = localStorage.getItem('api_key') || ''
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -117,12 +130,19 @@ async function fetchSettings() {
async function handleSave() { async function handleSave() {
saving.value = true saving.value = true
try { try {
if (settings.api_key) {
localStorage.setItem('api_key', settings.api_key)
} else {
localStorage.removeItem('api_key')
}
const { api_key, ...settingsToSend } = settings
const response = await fetch('http://localhost:8923/api/settings', { const response = await fetch('http://localhost:8923/api/settings', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(settings) body: JSON.stringify(settingsToSend)
}) })
if (response.ok) { if (response.ok) {
@@ -171,6 +191,13 @@ onMounted(() => {
color: var(--text-secondary); color: var(--text-secondary);
} }
.setting-hint {
margin-top: 8px;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.btn-icon { .btn-icon {
font-size: 20px; font-size: 20px;
margin-right: 8px; margin-right: 8px;

View File

@@ -22,6 +22,7 @@ class TasksManager:
'current_url': None, 'current_url': None,
'plugins': [] 'plugins': []
} }
self.estimated_total = 1000
def set_callbacks(self, progress_callback: Optional[Callable] = None, status_callback: Optional[Callable] = None): def set_callbacks(self, progress_callback: Optional[Callable] = None, status_callback: Optional[Callable] = None):
self.progress_callback = progress_callback self.progress_callback = progress_callback
@@ -34,6 +35,10 @@ class TasksManager:
if 'found' in data and 'verified' in data: if 'found' in data and 'verified' in data:
data['success_rate'] = round((data['verified'] / data['found'] * 100), 2) if data['found'] > 0 else 0 data['success_rate'] = round((data['verified'] / data['found'] * 100), 2) if data['found'] > 0 else 0
if 'found' in data:
data['current'] = data['found'] + self.stats['total_verified']
data['total'] = self.estimated_total
await self.progress_callback(data) await self.progress_callback(data)
async def _notify_status(self, status: str, message: str): async def _notify_status(self, status: str, message: str):
@@ -45,7 +50,7 @@ class TasksManager:
}) })
async def run_crawler(self): async def run_crawler(self):
await self._notify_status('crawling', '开始爬取代理啦~') await self._notify_status('crawling_start', '开始爬取代理啦~')
manager = PluginManager() manager = PluginManager()
count = 0 count = 0
@@ -59,11 +64,13 @@ class TasksManager:
count += 1 count += 1
self.stats['total_found'] = count self.stats['total_found'] = count
if count % 10 == 0: if count % 5 == 0:
await self._notify_progress({ await self._notify_progress({
'type': 'crawling', 'type': 'crawling',
'found': count, 'found': count,
'verified': self.stats['total_verified'] 'verified': self.stats['total_verified'],
'current_proxy': f"{ip}:{port}",
'message': f'正在爬取:已发现 {count} 个代理'
}) })
if self.stop_requested: if self.stop_requested:
@@ -73,7 +80,7 @@ class TasksManager:
logger.info(f"爬虫抓取阶段完成,共发现 {count} 个潜在代理。") logger.info(f"爬虫抓取阶段完成,共发现 {count} 个潜在代理。")
async def run_validator(self, db: SQLiteManager, validator: ProxyValidator): async def run_validator(self, db: SQLiteManager, validator: ProxyValidator):
await self._notify_status('validating', '开始验证代理啦~') await self._notify_status('validating_start', '开始验证代理啦~')
verified_count = 0 verified_count = 0
while True: while True:
@@ -91,12 +98,12 @@ class TasksManager:
verified_count += 1 verified_count += 1
self.stats['total_verified'] = verified_count self.stats['total_verified'] = verified_count
if verified_count % 5 == 0:
await self._notify_progress({ await self._notify_progress({
'type': 'validating', 'type': 'validating',
'found': self.stats['total_found'], 'found': self.stats['total_found'],
'verified': verified_count, 'verified': verified_count,
'current_proxy': f"{ip}:{port}" 'current_proxy': f"{ip}:{port}",
'message': f'正在验证:已验证 {verified_count} 个代理'
}) })
else: else:
logger.info(f"验证失败: {ip}:{port} ({protocol})") logger.info(f"验证失败: {ip}:{port} ({protocol})")
@@ -126,6 +133,8 @@ class TasksManager:
'plugins': [] 'plugins': []
} }
await self._notify_status('connecting', '正在连接插件源...')
await self._notify_status('starting', '正在启动爬虫...')
await self._notify_status('running', '任务开始啦~') await self._notify_status('running', '任务开始啦~')
async with ProxyValidator(max_concurrency=200) as validator: async with ProxyValidator(max_concurrency=200) as validator:

724
test_results.json Normal file
View File

@@ -0,0 +1,724 @@
{
"summary": {
"total_tests": 29,
"passed_tests": 29,
"failed_tests": 0,
"pass_rate": 100.0,
"timestamp": "2026-01-27T23:11:59.292107"
},
"results": [
{
"test_name": "GET / - 根路径访问",
"passed": true,
"message": "根路径返回正常",
"timestamp": "2026-01-27T23:11:21.092484",
"response_data": {
"message": "欢迎使用代理池API~",
"status": "running",
"data": null
}
},
{
"test_name": "GET /health - 健康检查",
"passed": true,
"message": "服务健康状态正常",
"timestamp": "2026-01-27T23:11:23.104732",
"response_data": {
"status": "healthy",
"timestamp": "2026-01-27T23:11:23.104732",
"database": "connected",
"version": "1.0.0"
}
},
{
"test_name": "GET /api/stats - 统计信息",
"passed": true,
"message": "成功获取统计信息,总数: 220",
"timestamp": "2026-01-27T23:11:25.116587",
"response_data": {
"code": 200,
"message": "获取统计信息成功啦~",
"data": {
"total": 220,
"available": 220,
"avg_score": 10.0,
"http_count": 147,
"https_count": 0,
"socks4_count": 73,
"socks5_count": 0,
"today_new": 220
}
}
},
{
"test_name": "GET /api/stats - 字段完整性",
"passed": true,
"message": "所有必需字段都存在",
"timestamp": "2026-01-27T23:11:25.116587",
"response_data": null
},
{
"test_name": "POST /api/proxies - 基本分页查询",
"passed": true,
"message": "成功获取代理列表,共 220 条",
"timestamp": "2026-01-27T23:11:27.126629",
"response_data": {
"code": 200,
"message": "获取代理列表成功啦~",
"data": {
"list": [
{
"ip": "120.26.68.107",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:23.000Z"
},
{
"ip": "169.61.46.13",
"port": 7563,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:23.000Z"
},
{
"ip": "35.209.198.222",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:21.000Z"
},
{
"ip": "34.81.160.132",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:21.000Z"
},
{
"ip": "176.126.164.213",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:19.000Z"
},
{
"ip": "8.220.136.174",
"port": 5060,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:15.000Z"
},
{
"ip": "47.86.53.59",
"port": 8080,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:14.000Z"
},
{
"ip": "40.177.106.156",
"port": 8080,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:10.000Z"
},
{
"ip": "47.56.110.204",
"port": 8989,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:10.000Z"
},
{
"ip": "193.53.127.169",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:09.000Z"
},
{
"ip": "163.172.167.48",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:08.000Z"
},
{
"ip": "36.67.136.27",
"port": 5678,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:08.000Z"
},
{
"ip": "162.223.90.144",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:08.000Z"
},
{
"ip": "104.197.218.238",
"port": 8080,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:59.000Z"
},
{
"ip": "211.230.49.122",
"port": 3128,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:54.000Z"
},
{
"ip": "159.195.84.83",
"port": 443,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:53.000Z"
},
{
"ip": "172.237.73.24",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:52.000Z"
},
{
"ip": "81.169.213.169",
"port": 8888,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:47.000Z"
},
{
"ip": "8.220.141.8",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:46.000Z"
},
{
"ip": "31.28.4.192",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:45.000Z"
}
],
"total": 220,
"page": 1,
"page_size": 20
}
}
},
{
"test_name": "POST /api/proxies - 基本分页查询 - 字段完整性",
"passed": true,
"message": "代理数据字段完整",
"timestamp": "2026-01-27T23:11:27.126629",
"response_data": null
},
{
"test_name": "POST /api/proxies - 带协议筛选",
"passed": true,
"message": "成功获取代理列表,共 147 条",
"timestamp": "2026-01-27T23:11:29.137101",
"response_data": {
"code": 200,
"message": "获取代理列表成功啦~",
"data": {
"list": [
{
"ip": "47.89.159.212",
"port": 1080,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:27.000Z"
},
{
"ip": "200.59.186.177",
"port": 999,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:26.000Z"
},
{
"ip": "34.76.142.148",
"port": 80,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:22.000Z"
},
{
"ip": "101.47.16.15",
"port": 7890,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:22.000Z"
},
{
"ip": "212.114.194.72",
"port": 80,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:21.000Z"
},
{
"ip": "8.213.156.191",
"port": 221,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:21.000Z"
},
{
"ip": "191.101.1.116",
"port": 80,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:20.000Z"
},
{
"ip": "51.141.175.118",
"port": 80,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:19.000Z"
},
{
"ip": "213.73.25.230",
"port": 8080,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:19.000Z"
},
{
"ip": "50.203.147.152",
"port": 80,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:10:17.000Z"
}
],
"total": 147,
"page": 1,
"page_size": 10
}
}
},
{
"test_name": "POST /api/proxies - 带协议筛选 - 字段完整性",
"passed": true,
"message": "代理数据字段完整",
"timestamp": "2026-01-27T23:11:29.137101",
"response_data": null
},
{
"test_name": "POST /api/proxies - 带分数筛选",
"passed": true,
"message": "成功获取代理列表,共 0 条",
"timestamp": "2026-01-27T23:11:31.148007",
"response_data": {
"code": 200,
"message": "获取代理列表成功啦~",
"data": {
"list": [],
"total": 0,
"page": 1,
"page_size": 10
}
}
},
{
"test_name": "POST /api/proxies - 带分数筛选 - 空列表",
"passed": true,
"message": "代理列表为空(可能数据库无数据)",
"timestamp": "2026-01-27T23:11:31.148007",
"response_data": {
"code": 200,
"message": "获取代理列表成功啦~",
"data": {
"list": [],
"total": 0,
"page": 1,
"page_size": 10
}
}
},
{
"test_name": "POST /api/proxies - 带排序",
"passed": true,
"message": "成功获取代理列表,共 221 条",
"timestamp": "2026-01-27T23:11:33.159151",
"response_data": {
"code": 200,
"message": "获取代理列表成功啦~",
"data": {
"list": [
{
"ip": "212.114.194.75",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:28.000Z"
},
{
"ip": "35.209.198.222",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:21.000Z"
},
{
"ip": "40.177.106.156",
"port": 8080,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:10.000Z"
},
{
"ip": "163.172.167.48",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:11:08.000Z"
},
{
"ip": "159.195.84.83",
"port": 443,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:53.000Z"
},
{
"ip": "31.28.4.192",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:45.000Z"
},
{
"ip": "108.170.12.10",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:44.000Z"
},
{
"ip": "35.180.127.14",
"port": 1001,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:42.000Z"
},
{
"ip": "139.162.200.213",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:42.000Z"
},
{
"ip": "154.90.48.76",
"port": 80,
"protocol": "socks4",
"score": 10,
"last_check": "2026-01-27T15:10:38.000Z"
}
],
"total": 221,
"page": 1,
"page_size": 10
}
}
},
{
"test_name": "POST /api/proxies - 带排序 - 字段完整性",
"passed": true,
"message": "代理数据字段完整",
"timestamp": "2026-01-27T23:11:33.159151",
"response_data": null
},
{
"test_name": "POST /api/proxies - 参数验证测试 - 无效协议",
"passed": true,
"message": "参数验证失败,符合预期",
"timestamp": "2026-01-27T23:11:35.168328",
"response_data": {
"detail": [
{
"type": "value_error",
"loc": [
"body",
"protocol"
],
"msg": "Value error, 协议类型必须是 http, https, socks4 或 socks5",
"input": "invalid",
"ctx": {
"error": {}
},
"url": "https://errors.pydantic.dev/2.12/v/value_error"
}
]
}
},
{
"test_name": "POST /api/proxies - 参数验证测试 - page为0",
"passed": true,
"message": "参数验证失败,符合预期",
"timestamp": "2026-01-27T23:11:37.176455",
"response_data": {
"detail": [
{
"type": "greater_than_equal",
"loc": [
"body",
"page"
],
"msg": "Input should be greater than or equal to 1",
"input": 0,
"ctx": {
"ge": 1
},
"url": "https://errors.pydantic.dev/2.12/v/greater_than_equal"
}
]
}
},
{
"test_name": "POST /api/proxies - 参数验证测试 - page_size超过100",
"passed": true,
"message": "参数验证失败,符合预期",
"timestamp": "2026-01-27T23:11:39.186465",
"response_data": {
"detail": [
{
"type": "less_than_equal",
"loc": [
"body",
"page_size"
],
"msg": "Input should be less than or equal to 100",
"input": 101,
"ctx": {
"le": 100
},
"url": "https://errors.pydantic.dev/2.12/v/less_than_equal"
}
]
}
},
{
"test_name": "GET /api/proxies/random - 获取随机代理",
"passed": true,
"message": "成功获取随机代理: 176.126.103.194:44214",
"timestamp": "2026-01-27T23:11:41.196335",
"response_data": {
"code": 200,
"message": "获取随机代理成功啦~",
"data": {
"ip": "176.126.103.194",
"port": 44214,
"protocol": "http",
"score": 10,
"last_check": "2026-01-27T15:08:12.000Z"
}
}
},
{
"test_name": "GET /api/proxies/有效代理",
"passed": true,
"message": "代理不存在(符合预期)",
"timestamp": "2026-01-27T23:11:43.202256",
"response_data": {
"code": 404,
"message": "代理不存在呢~",
"data": null
}
},
{
"test_name": "GET /api/proxies/不存在的代理",
"passed": true,
"message": "代理不存在(符合预期)",
"timestamp": "2026-01-27T23:11:45.210946",
"response_data": {
"code": 404,
"message": "代理不存在呢~",
"data": null
}
},
{
"test_name": "GET /api/proxies/export/csv - 导出CSV格式",
"passed": true,
"message": "成功导出CSV格式内容长度: 552",
"timestamp": "2026-01-27T23:11:47.221104",
"response_data": {
"content_length": 552
}
},
{
"test_name": "GET /api/proxies/export/csv - CSV格式验证",
"passed": true,
"message": "CSV格式正确包含表头",
"timestamp": "2026-01-27T23:11:47.221104",
"response_data": null
},
{
"test_name": "GET /api/proxies/export/txt - 导出TXT格式",
"passed": true,
"message": "成功导出TXT格式内容长度: 184",
"timestamp": "2026-01-27T23:11:49.226991",
"response_data": {
"content_length": 184
}
},
{
"test_name": "GET /api/proxies/export/txt - TXT格式验证",
"passed": true,
"message": "TXT格式正确",
"timestamp": "2026-01-27T23:11:49.228522",
"response_data": null
},
{
"test_name": "GET /api/proxies/export/json - 导出JSON格式",
"passed": true,
"message": "成功导出JSON格式内容长度: 1260",
"timestamp": "2026-01-27T23:11:51.242429",
"response_data": {
"content_length": 1260
}
},
{
"test_name": "GET /api/proxies/export/json - JSON格式验证",
"passed": true,
"message": "JSON格式正确",
"timestamp": "2026-01-27T23:11:51.244593",
"response_data": null
},
{
"test_name": "GET /api/proxies/export/invalid - 无效格式测试",
"passed": true,
"message": "正确返回400错误",
"timestamp": "2026-01-27T23:11:53.258979",
"response_data": null
},
{
"test_name": "GET /api/crawler/status - 获取爬虫状态",
"passed": true,
"message": "爬虫状态: 运行中",
"timestamp": "2026-01-27T23:11:55.270148",
"response_data": {
"code": 200,
"message": "获取爬虫状态成功啦~",
"data": {
"running": true,
"stats": {
"total_found": 5524,
"total_verified": 4,
"start_time": "2026-01-27T23:06:12.013714",
"current_url": null,
"plugins": [
"IP3366",
"89免费代理",
"快代理",
"ProxyListDownload",
"SpeedX代理源",
"云代理"
]
}
}
}
},
{
"test_name": "GET /api/scheduler - 获取定时任务状态",
"passed": true,
"message": "定时任务状态: 未启用",
"timestamp": "2026-01-27T23:11:57.282485",
"response_data": {
"code": 200,
"message": "获取定时任务状态成功啦~",
"data": {
"enabled": false,
"interval_minutes": 60
}
}
},
{
"test_name": "GET /api/plugins - 获取插件列表",
"passed": true,
"message": "成功获取插件列表,共 6 个插件",
"timestamp": "2026-01-27T23:11:59.290536",
"response_data": {
"code": 200,
"message": "获取插件列表成功啦~",
"data": {
"plugins": [
{
"id": "IP3366",
"name": "IP3366",
"enabled": true,
"description": "从IP3366网站爬取代理",
"last_run": null,
"success_count": 0,
"failure_count": 0
},
{
"id": "89免费代理",
"name": "89免费代理",
"enabled": true,
"description": "从89免费代理网站爬取代理",
"last_run": null,
"success_count": 0,
"failure_count": 0
},
{
"id": "快代理",
"name": "快代理",
"enabled": true,
"description": "从快代理网站爬取代理",
"last_run": null,
"success_count": 0,
"failure_count": 0
},
{
"id": "ProxyListDownload",
"name": "ProxyListDownload",
"enabled": true,
"description": "从ProxyListDownload网站爬取代理",
"last_run": null,
"success_count": 0,
"failure_count": 0
},
{
"id": "SpeedX代理源",
"name": "SpeedX代理源",
"enabled": true,
"description": "从SpeedX代理源网站爬取代理",
"last_run": null,
"success_count": 0,
"failure_count": 0
},
{
"id": "云代理",
"name": "云代理",
"enabled": true,
"description": "从云代理网站爬取代理",
"last_run": null,
"success_count": 0,
"failure_count": 0
}
]
}
}
},
{
"test_name": "GET /api/plugins - 插件字段完整性",
"passed": true,
"message": "插件数据字段完整",
"timestamp": "2026-01-27T23:11:59.290536",
"response_data": null
}
]
}