diff --git a/app/.env b/app/.env index 6071276..f045f4c 100644 --- a/app/.env +++ b/app/.env @@ -14,4 +14,7 @@ EIP_DEFAULT_NUM=1 REDIS_URL=redis://127.0.0.1:6379/5 # 日志级别 -LOG_LEVEL=INFO \ No newline at end of file +LOG_LEVEL=INFO + +# 端口数==条数 从172.30.168.2开始 +PORT_NUM=3 \ No newline at end of file diff --git a/app/config.py b/app/config.py index 890d29d..7f00586 100644 --- a/app/config.py +++ b/app/config.py @@ -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" diff --git a/app/line_status_manager.py b/app/line_status_manager.py new file mode 100644 index 0000000..64c26a2 --- /dev/null +++ b/app/line_status_manager.py @@ -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() diff --git a/app/rotation_service.py b/app/rotation_service.py index c7d832c..eea06e5 100644 --- a/app/rotation_service.py +++ b/app/rotation_service.py @@ -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] diff --git a/app/routers/proxy.py b/app/routers/proxy.py index 35ed168..13b7f64 100644 --- a/app/routers/proxy.py +++ b/app/routers/proxy.py @@ -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} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index d57be75..1c4a592 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -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; } @@ -198,11 +266,6 @@
| 序号 | +端口号 | +当前IP | +状态 | +轮换次数 | +最后轮换时间 | +操作 | +
|---|
正在执行IP轮换,请稍候...
+正在加载线路信息...