功能完善

This commit is contained in:
wangqifan 2025-10-22 14:46:29 +08:00
parent 5d61a240ab
commit eda8e0bfcb
7 changed files with 488 additions and 80 deletions

View File

@ -15,3 +15,6 @@ REDIS_URL=redis://127.0.0.1:6379/5
# 日志级别
LOG_LEVEL=INFO
# 端口数==条数 从172.30.168.2开始
PORT_NUM=3

View File

@ -15,18 +15,26 @@ class Settings(BaseModel):
eip_default_num: int = int(os.getenv("EIP_DEFAULT_NUM", "10"))
redis_url: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
log_level: str = os.getenv("LOG_LEVEL", "INFO")
port_num: int = int(os.getenv("PORT_NUM", 3))
settings = Settings()
client_infos={}
for i in range(230):
for i in range(settings.port_num):
ip4 = 2+i
ip = '172.30.168.'+str(ip4)
client_infos[str(i+1)] = ip
port_mapping = {}
for i in range(settings.port_num):
num = i+1
port = 44000 + num
port_mapping[num] = port
city_dict = {
"上海": {
"上海": "f8a5e9b04178490cf8e71f5b273538aa75ef2c978ecac974a176d93af966ef53"

174
app/line_status_manager.py Normal file
View File

@ -0,0 +1,174 @@
"""
线路状态管理模块
负责保存和读取每个线路的最新状态信息到文件中
"""
import json
import os
from datetime import datetime
from typing import Dict, List, Optional, Any
from pathlib import Path
from .config import client_infos
class LineStatusManager:
"""线路状态管理器"""
def __init__(self, status_file: str = "line_status.json"):
"""
初始化状态管理器
Args:
status_file: 状态文件路径
"""
self.status_file = Path(status_file)
self._ensure_status_file()
def _ensure_status_file(self):
"""确保状态文件存在"""
if not self.status_file.exists():
self._save_status({})
def _load_status(self) -> Dict[str, Any]:
"""从文件加载状态"""
try:
with open(self.status_file, 'r', encoding='utf-8') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def _save_status(self, status: Dict[str, Any]):
"""保存状态到文件"""
with open(self.status_file, 'w', encoding='utf-8') as f:
json.dump(status, f, ensure_ascii=False, indent=2)
def get_line_status(self, line_id: int) -> Dict[str, Any]:
"""
获取指定线路的状态
Args:
line_id: 线路ID
Returns:
Dict[str, Any]: 线路状态信息
"""
status = self._load_status()
line_key = str(line_id)
if line_key not in status:
return {
"id": line_id,
"current_ip": None,
"last_rotate_time": None,
"status": "inactive",
"edge_device": None,
"geo_location": None,
"rotate_count": 0
}
return status[line_key]
def update_line_status(self, line_id: int, **kwargs):
"""
更新线路状态
Args:
line_id: 线路ID
**kwargs: 要更新的状态字段
"""
status = self._load_status()
line_key = str(line_id)
if line_key not in status:
status[line_key] = {
"id": line_id,
"current_ip": None,
"last_rotate_time": None,
"status": "inactive",
"edge_device": None,
"geo_location": None,
"rotate_count": 0
}
# 更新字段
for key, value in kwargs.items():
status[line_key][key] = value
# 更新时间戳
status[line_key]["last_update_time"] = datetime.now().isoformat()
self._save_status(status)
def get_all_lines_status(self) -> List[Dict[str, Any]]:
"""
获取所有线路状态仅已保存的状态
Returns:
List[Dict[str, Any]]: 所有线路状态列表按线路ID排序
"""
status = self._load_status()
lines = list(status.values())
# 按线路ID排序
lines.sort(key=lambda x: x.get("id", 0))
return lines
def get_all_lines_with_defaults(self) -> List[Dict[str, Any]]:
"""
获取所有线路状态包括默认配置的线路
Returns:
List[Dict[str, Any]]: 所有线路状态列表包括未配置的线路
"""
# 获取已保存的状态
saved_status = self._load_status()
lines = []
# 遍历所有配置的线路
for port_id, default_ip in client_infos.items():
line_id = int(port_id)
line_key = str(line_id)
if line_key in saved_status:
# 使用已保存的状态
line_data = saved_status[line_key].copy()
else:
# 使用默认状态
line_data = {
"id": line_id,
"current_ip": default_ip,
"last_rotate_time": None,
"status": "inactive",
"edge_device": None,
"geo_location": None,
"rotate_count": 0
}
lines.append(line_data)
# 按线路ID排序
lines.sort(key=lambda x: x.get("id", 0))
return lines
def increment_rotate_count(self, line_id: int):
"""增加轮换次数计数"""
current_status = self.get_line_status(line_id)
self.update_line_status(
line_id,
rotate_count=current_status.get("rotate_count", 0) + 1
)
# 创建全局状态管理器实例
status_manager = LineStatusManager()
# 使用示例:
# 获取所有线路状态(仅已保存的)
# all_lines = status_manager.get_all_lines_status()
#
# 获取所有线路状态(包括默认配置)
# all_lines_with_defaults = status_manager.get_all_lines_with_defaults()

View File

@ -12,9 +12,10 @@ IP轮换服务模块
import random
from typing import Any, Dict, List, Optional, Tuple
from .config import settings, city_dict, client_infos
from .config import settings, city_dict, client_infos, port_mapping
from .eip_client import client_singleton as eip
from .redis_store import store_singleton as kv
from .line_status_manager import status_manager
def _extract_device_ip_and_id(device: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
@ -77,15 +78,29 @@ def apply_gateway_route(edge_id: Optional[str], ip: str, geo: str, client_id:str
Returns:
Dict[str, Any]: 网关配置设置结果
"""
# 创建路由规则配置
rule = {
"table": 1, # 路由表ID
"enable": True, # 启用规则
"edge": [edge_id] if edge_id else [], # 边缘设备列表
"network": [client_infos[str(client_id)]], # 网络配置(当前为空)
"cityhash": geo or "", # 城市哈希值
}
config = {"id": 1, "rules": [rule]} # 配置ID和规则列表
lines = status_manager.get_all_lines_status()
rule = []
for line in lines:
if line['id'] == client_id:
rule.append({
"table": client_id, # 路由表ID
"enable": True, # 启用规则
"edge": [edge_id] if edge_id else [], # 边缘设备列表
"network": [client_infos[str(client_id)]], # 网络配置(当前为空)
"cityhash": geo or "", # 城市哈希值
})
else:
rule.append({
"table": line['id'], # 路由表ID
"enable": True, # 启用规则
"edge": [line['edge_device']], # 边缘设备列表
"network": [client_infos[str(line['id'])]], # 网络配置(当前为空)
"cityhash": line['geo_location'], # 城市哈希值
})
config = {"id": 1, "rules": rule} # 配置ID和规则列表
return eip.gateway_config_set(settings.eip_gateway_mac, config)
@ -212,4 +227,5 @@ def get_random_cityhash(cities) -> str:
city_info = get_random_city(cities)
return city_info["cityhash"]
def get_port(line_id):
return port_mapping[line_id]

View File

@ -3,7 +3,7 @@ from typing import Optional
from fastapi import APIRouter, Body, Query, Form,Request
from pydantic import BaseModel
from ..rotation_service import rotate as rotate_impl, status as status_impl ,citie_list as cities_impl
from ..rotation_service import rotate as rotate_impl, status as status_impl ,citie_list as cities_impl,get_port
router = APIRouter()
@ -24,6 +24,22 @@ def rotate(
# effective_cityhash = req.cityhash
# effective_num = req.num
result = rotate_impl(client_id=client_id,cities =cities)
# 如果轮换成功,更新线路状态
if result.get("changed", False):
from ..line_status_manager import status_manager
from datetime import datetime
status_manager.update_line_status(
client_id,
current_ip=result.get("ip"),
last_rotate_time=datetime.now().isoformat(),
status="active",
edge_device=result.get("edge"),
geo_location=result.get("geo")
)
status_manager.increment_rotate_count(client_id)
return result
@ -34,3 +50,27 @@ def get_status():
@router.get("/cities")
def get_cities():
return cities_impl()
@router.get("/lines")
def get_lines():
"""获取所有线路信息"""
from ..line_status_manager import status_manager
# 使用新的方法获取所有线路状态
lines_data = status_manager.get_all_lines_with_defaults()
# 格式化返回数据
lines = []
for line_data in lines_data:
lines.append({
"id": line_data["id"],
"port": get_port(line_data["id"]),
"ip": line_data.get("current_ip"),
"status": line_data.get("status", "active"),
"last_rotate_time": line_data.get("last_rotate_time"),
"edge_device": line_data.get("edge_device"),
"geo_location": line_data.get("geo_location"),
"rotate_count": line_data.get("rotate_count", 0)
})
return {"data": lines}

View File

@ -19,7 +19,7 @@
}
.container {
max-width: 800px;
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
@ -61,18 +61,6 @@
font-size: 1.1em;
}
.client-id {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #4facfe;
margin-bottom: 20px;
}
.client-id strong {
color: #4facfe;
}
.checkbox-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@ -103,26 +91,80 @@
color: #495057;
}
.btn-container {
.select-all-container {
margin-bottom: 15px;
padding: 10px;
background: #e3f2fd;
border-radius: 6px;
border-left: 4px solid #2196f3;
}
.select-all-container input[type="checkbox"] {
transform: scale(1.3);
margin-right: 10px;
}
.select-all-container label {
font-weight: 600;
color: #1976d2;
}
.table-container {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.table-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
margin-top: 30px;
}
.table-header h2 {
font-size: 1.5em;
font-weight: 300;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 15px;
text-align: center;
border-bottom: 1px solid #e9ecef;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
font-size: 1.1em;
}
tr:hover {
background: #f8f9fa;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 40px;
font-size: 1.1em;
border-radius: 50px;
padding: 8px 16px;
font-size: 0.9em;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
@ -131,9 +173,20 @@
transform: none;
}
.btn.rotating {
background: #6c757d;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.result {
margin-top: 30px;
padding: 20px;
margin-top: 20px;
padding: 15px;
border-radius: 8px;
display: none;
}
@ -171,22 +224,37 @@
100% { transform: rotate(360deg); }
}
.select-all-container {
margin-bottom: 15px;
padding: 10px;
background: #e3f2fd;
border-radius: 6px;
border-left: 4px solid #2196f3;
}
.select-all-container input[type="checkbox"] {
transform: scale(1.3);
margin-right: 10px;
}
.select-all-container label {
.status-active {
color: #28a745;
font-weight: 600;
}
.status-inactive {
color: #dc3545;
font-weight: 600;
}
.ip-address {
font-family: 'Courier New', monospace;
background: #e9ecef;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
}
.rotate-count {
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 4px;
font-weight: 600;
font-size: 0.9em;
}
.last-rotate-time {
font-size: 0.85em;
color: #666;
font-family: 'Courier New', monospace;
}
</style>
</head>
@ -198,11 +266,6 @@
</div>
<div class="content">
<!-- 客户端ID显示 -->
<div class="client-id">
<strong>客户端ID:</strong> <span id="client-id">{{ client_id }}</span>
</div>
<!-- 省份选择 -->
<div class="form-group">
<label>选择省份(默认全选)</label>
@ -215,17 +278,33 @@
</div>
</div>
<!-- 操作按钮 -->
<div class="btn-container">
<button class="btn" id="rotate-btn" onclick="performRotate()">
🔄 开始IP轮换
</button>
<!-- 线路信息表格 -->
<div class="table-container">
<div class="table-header">
<h2>线路信息管理</h2>
</div>
<table>
<thead>
<tr>
<th>序号</th>
<th>端口号</th>
<th>当前IP</th>
<th>状态</th>
<th>轮换次数</th>
<th>最后轮换时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="lines-table-body">
<!-- 线路数据将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
<!-- 加载状态 -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>正在执行IP轮换请稍候...</p>
<p>正在加载线路信息...</p>
</div>
<!-- 结果显示 -->
@ -234,12 +313,14 @@
</div>
<script>
let linesData = [];
let allProvinces = [];
let selectedProvinces = [];
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadProvinces();
loadLines();
setupSelectAll();
});
@ -299,19 +380,58 @@
selectAllCheckbox.checked = selectedProvinces.length === allProvinces.length;
}
// 执行IP轮换
async function performRotate() {
if (selectedProvinces.length === 0) {
showResult('请至少选择一个省份!', 'error');
return;
// 加载线路列表
async function loadLines() {
try {
document.getElementById('loading').style.display = 'block';
const response = await fetch('/proxy/lines');
const data = await response.json();
linesData = data.data;
renderLines();
} catch (error) {
console.error('加载线路失败:', error);
showResult('加载线路信息失败: ' + error.message, 'error');
} finally {
document.getElementById('loading').style.display = 'none';
}
}
const clientId = document.getElementById('client-id').textContent;
// 渲染线路表格
function renderLines() {
const tbody = document.getElementById('lines-table-body');
tbody.innerHTML = '';
linesData.forEach((line, index) => {
const row = document.createElement('tr');
const lastRotateTime = line.last_rotate_time ?
new Date(line.last_rotate_time).toLocaleString('zh-CN') : '从未轮换';
row.innerHTML = `
<td>${index + 1}</td>
<td>${line.port}</td>
<td><span class="ip-address">${line.ip}</span></td>
<td><span class="status-${line.status}">${line.status === 'active' ? '活跃' : '离线'}</span></td>
<td><span class="rotate-count">${line.rotate_count || 0}</span></td>
<td><span class="last-rotate-time">${lastRotateTime}</span></td>
<td>
<button class="btn" onclick="rotateLine(${line.id})" id="btn-${line.id}">
🔄 换IP
</button>
</td>
`;
tbody.appendChild(row);
});
}
// 执行单条线路IP轮换
async function rotateLine(lineId) {
const button = document.getElementById(`btn-${lineId}`);
const originalText = button.innerHTML;
// 显示加载状态
document.getElementById('loading').style.display = 'block';
document.getElementById('rotate-btn').disabled = true;
document.getElementById('result').style.display = 'none';
button.disabled = true;
button.classList.add('rotating');
button.innerHTML = '⏳ 轮换中...';
try {
const response = await fetch('/proxy/rotate', {
@ -320,8 +440,8 @@
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: parseInt(clientId),
citys: selectedProvinces.join(',')
id: lineId,
citys: selectedProvinces.join(',') // 使用选中的省份
})
});
@ -329,24 +449,32 @@
if (result.changed) {
showResult(`
<h3>✅ IP轮换成功</h3>
<h3>线路 ${lineId} IP轮换成功</h3>
<p><strong>新IP地址:</strong> ${result.ip}</p>
<p><strong>边缘设备:</strong> ${result.edge}</p>
<p><strong>网关状态:</strong> 已更新</p>
<p><strong>地理位置:</strong> ${result.geo}</p>
`, 'success');
// 更新表格中的IP地址
const line = linesData.find(l => l.id === lineId);
if (line) {
line.ip = result.ip;
renderLines(); // 重新渲染表格
}
} else {
showResult(`
<h3>⚠️ IP轮换失败</h3>
<h3>⚠️ 线路 ${lineId} IP轮换失败</h3>
<p><strong>原因:</strong> ${result.reason}</p>
`, 'error');
}
} catch (error) {
console.error('轮换失败:', error);
showResult('IP轮换失败: ' + error.message, 'error');
showResult('线路 ' + lineId + ' IP轮换失败: ' + error.message, 'error');
} finally {
// 隐藏加载状态
document.getElementById('loading').style.display = 'none';
document.getElementById('rotate-btn').disabled = false;
// 恢复按钮状态
button.disabled = false;
button.classList.remove('rotating');
button.innerHTML = originalText;
}
}
@ -356,6 +484,13 @@
resultDiv.innerHTML = message;
resultDiv.className = `result ${type}`;
resultDiv.style.display = 'block';
// 3秒后自动隐藏成功消息
if (type === 'success') {
setTimeout(() => {
resultDiv.style.display = 'none';
}, 3000);
}
}
</script>
</body>

32
line_status.json Normal file
View File

@ -0,0 +1,32 @@
{
"1": {
"id": 1,
"current_ip": "222.169.52.224",
"last_rotate_time": "2025-10-22T14:45:11.633459",
"status": "active",
"edge_device": "DCD87C27D4D6",
"geo_location": "a4002c19888160b7f7875e7d5727ad8a0c7323af8070f39dcf60895112ac1283",
"rotate_count": 8,
"last_update_time": "2025-10-22T14:45:11.645492"
},
"2": {
"id": 2,
"current_ip": "39.128.153.88",
"last_rotate_time": "2025-10-22T14:41:24.720185",
"status": "active",
"edge_device": "DCD87C29EB38",
"geo_location": "6dafce691415eeb72b86b9529e6d05773cf7e94df13957d3df64b37267cc2fe8",
"rotate_count": 5,
"last_update_time": "2025-10-22T14:41:24.729735"
},
"3": {
"id": 3,
"current_ip": "119.55.16.172",
"last_rotate_time": "2025-10-22T14:34:16.421955",
"status": "active",
"edge_device": "DCD87C44F885",
"geo_location": "bc41d34110c2989b29f7521133d605e7a8aa6e9edb488217c5c0d087603a753c",
"rotate_count": 2,
"last_update_time": "2025-10-22T14:34:16.436191"
}
}