加了前端
This commit is contained in:
parent
102ac41025
commit
5d61a240ab
287
README.md
287
README.md
@ -1,74 +1,257 @@
|
|||||||
## JD EIP 代理轮换服务 (FastAPI)
|
# 🌐 EIP IP轮换服务
|
||||||
|
|
||||||
基于京东 EIP API 的代理 IP 自动轮换后端。支持:
|
一个基于FastAPI的智能IP地址轮换管理系统,支持多省份选择、自动IP切换和Web控制面板。
|
||||||
- 鉴权获取 `X-Token`
|
|
||||||
- 查询城市与设备
|
|
||||||
- 设置网关链路,自动切换到未使用过的 IP(按天去重)
|
|
||||||
- 使用 Redis 存储每日已使用 IP 与状态
|
|
||||||
|
|
||||||
### 运行
|
## ✨ 功能特性
|
||||||
1. 创建并编辑 `.env`(参考 `.env.example`)
|
|
||||||
2. 安装依赖:
|
### 🔄 核心功能
|
||||||
|
- **智能IP轮换**: 自动从可用设备中选择未使用的IP地址
|
||||||
|
- **多省份支持**: 支持上海、四川、广东等多个省份的城市选择
|
||||||
|
- **随机城市选择**: 从指定省份中随机选择城市进行IP轮换
|
||||||
|
- **使用记录追踪**: 基于Redis的IP使用记录,避免重复使用
|
||||||
|
- **网关路由配置**: 自动配置网关路由规则
|
||||||
|
|
||||||
|
### 🎯 Web控制面板
|
||||||
|
- **现代化UI**: 响应式设计,支持移动端访问
|
||||||
|
- **省份选择**: 可视化省份选择,支持全选/取消全选
|
||||||
|
- **实时状态**: 显示当前IP、使用统计和网关状态
|
||||||
|
- **URL参数支持**: 通过URL参数传递客户端ID
|
||||||
|
|
||||||
|
### 🔧 技术特性
|
||||||
|
- **自动重试**: 网络请求失败时自动重试,支持指数退避
|
||||||
|
- **Token管理**: 自动处理认证令牌的获取和刷新
|
||||||
|
- **容错处理**: 兼容多种后端响应格式
|
||||||
|
- **配置管理**: 支持环境变量和配置文件
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- Python 3.8+
|
||||||
|
- Redis服务器
|
||||||
|
- EIP服务访问权限
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
3. 启动:
|
|
||||||
|
### 配置环境变量
|
||||||
|
创建 `app/.env` 文件:
|
||||||
|
```env
|
||||||
|
# EIP服务配置
|
||||||
|
EIP_BASE_URL=https://smart.jdbox.xyz:58001
|
||||||
|
EIP_USERNAME=your_username
|
||||||
|
EIP_PASSWORD=your_password
|
||||||
|
EIP_GATEWAY_MAC=your_gateway_mac
|
||||||
|
EIP_DEFAULT_CITYHASH=your_default_cityhash
|
||||||
|
EIP_DEFAULT_NUM=10
|
||||||
|
|
||||||
|
# Redis配置
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
|
# 日志级别
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动服务
|
||||||
```bash
|
```bash
|
||||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
# 开发模式
|
||||||
|
python -m app.main
|
||||||
|
|
||||||
|
# 或使用uvicorn
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
### 目录结构
|
### 访问Web界面
|
||||||
```
|
- **主页**: http://localhost:8000/
|
||||||
app/
|
- **带客户端ID**: http://localhost:8000/?id=123
|
||||||
config.py # 配置与环境变量
|
- **API文档**: http://localhost:8000/docs
|
||||||
eip_client.py # 与 JD EIP API 交互
|
|
||||||
redis_store.py # Redis 存取封装
|
## 📚 API接口
|
||||||
rotation_service.py # 轮换逻辑
|
|
||||||
routers/
|
### 核心接口
|
||||||
proxy.py # /proxy API 路由
|
|
||||||
main.py # FastAPI 入口
|
#### 1. IP轮换
|
||||||
|
```http
|
||||||
|
POST /proxy/rotate
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"citys": "上海,四川,广东"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### API 概览
|
**响应示例**:
|
||||||
- GET `/health` 健康检查
|
|
||||||
- POST `/proxy/rotate` 执行轮换(可传 cityhash/num)
|
|
||||||
- GET `/proxy/status` 当前状态
|
|
||||||
|
|
||||||
#### POST `/proxy/rotate`
|
|
||||||
- 请求体(JSON):
|
|
||||||
- `cityhash`(可选,string):城市哈希;若不传,将使用环境变量 `EIP_DEFAULT_CITYHASH`。
|
|
||||||
- `num`(可选,int):拉取设备数量上限;若不传,使用环境变量 `EIP_DEFAULT_NUM`,再不设则默认 10。
|
|
||||||
|
|
||||||
- 行为说明:
|
|
||||||
- 根据 `cityhash` 调用设备列表接口(数量为 `num`),从中选择「今天未使用过」的第一个 IP。
|
|
||||||
- 将选中设备的 `edge` 配置到网关规则中,并把该 IP 记录为当天已使用。
|
|
||||||
- 若没有可用且今天未使用的 IP,则返回未变更原因。
|
|
||||||
|
|
||||||
- 请求示例:
|
|
||||||
```bash
|
|
||||||
curl -X POST 'http://localhost:8000/proxy/rotate' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"id": 1}'
|
|
||||||
```
|
|
||||||
|
|
||||||
- 响应示例(变更成功):
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"changed": true,
|
"changed": true,
|
||||||
"ip": "1.2.3.4",
|
"ip": "192.168.1.100",
|
||||||
"edge": "edge-id-xxx",
|
"edge": "edge_device_001",
|
||||||
"status": { "...": "gateway_status_payload" }
|
"status": {
|
||||||
|
"gateway_status": "active"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- 响应示例(无可用 IP):
|
#### 2. 获取状态
|
||||||
|
```http
|
||||||
|
GET /proxy/status
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"changed": false,
|
"current": {
|
||||||
"reason": "没有可用且今天未使用的 IP"
|
"ip": "192.168.1.100",
|
||||||
|
"edge_id": "edge_device_001"
|
||||||
|
},
|
||||||
|
"used_today": 5,
|
||||||
|
"gateway": {
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 备注
|
#### 3. 获取城市列表
|
||||||
- EIP 详细接口见 `API.md`
|
```http
|
||||||
|
GET /proxy/cities
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": ["上海", "四川", "广东"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
```http
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
jdeip/
|
||||||
|
├── app/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── main.py # FastAPI应用入口
|
||||||
|
│ ├── config.py # 配置管理
|
||||||
|
│ ├── eip_client.py # EIP客户端
|
||||||
|
│ ├── rotation_service.py # IP轮换服务
|
||||||
|
│ ├── redis_store.py # Redis存储
|
||||||
|
│ ├── routers/
|
||||||
|
│ │ └── proxy.py # API路由
|
||||||
|
│ └── templates/
|
||||||
|
│ └── index.html # Web控制面板
|
||||||
|
├── requirements.txt # 依赖包
|
||||||
|
├── README.md # 项目文档
|
||||||
|
└── .env # 环境变量配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 核心模块
|
||||||
|
|
||||||
|
### EIP客户端 (`eip_client.py`)
|
||||||
|
- 处理与EIP服务的所有通信
|
||||||
|
- 自动认证和令牌管理
|
||||||
|
- 支持设备列表、网关配置等操作
|
||||||
|
- 内置重试机制和错误处理
|
||||||
|
|
||||||
|
### 轮换服务 (`rotation_service.py`)
|
||||||
|
- 核心IP轮换逻辑
|
||||||
|
- 随机城市选择算法
|
||||||
|
- 使用记录管理
|
||||||
|
- 网关路由配置
|
||||||
|
|
||||||
|
### Redis存储 (`redis_store.py`)
|
||||||
|
- IP使用记录存储
|
||||||
|
- 按天统计使用情况
|
||||||
|
- 当前状态管理
|
||||||
|
|
||||||
|
### Web控制面板
|
||||||
|
- 现代化响应式界面
|
||||||
|
- 省份可视化选择
|
||||||
|
- 实时状态显示
|
||||||
|
- 异步操作支持
|
||||||
|
|
||||||
|
## 🎨 Web界面功能
|
||||||
|
|
||||||
|
### 主要特性
|
||||||
|
- **省份选择**: 支持多选省份,默认全选
|
||||||
|
- **客户端ID**: 通过URL参数传递,支持 `?id=123`
|
||||||
|
- **实时轮换**: 一键执行IP轮换操作
|
||||||
|
- **状态显示**: 显示轮换结果和错误信息
|
||||||
|
- **响应式设计**: 支持桌面和移动设备
|
||||||
|
|
||||||
|
### 使用方式
|
||||||
|
1. 访问 `http://localhost:8000/` 进入控制面板
|
||||||
|
2. 选择需要的省份(默认全选)
|
||||||
|
3. 点击"开始IP轮换"按钮
|
||||||
|
4. 查看轮换结果
|
||||||
|
|
||||||
|
## 🔒 安全特性
|
||||||
|
|
||||||
|
- **环境变量**: 敏感信息通过环境变量管理
|
||||||
|
- **Token自动刷新**: 避免令牌过期问题
|
||||||
|
- **错误处理**: 完善的异常处理机制
|
||||||
|
- **输入验证**: 严格的参数验证
|
||||||
|
|
||||||
|
## 📊 监控和日志
|
||||||
|
|
||||||
|
- **健康检查**: `/health` 端点提供服务状态
|
||||||
|
- **使用统计**: Redis记录IP使用情况
|
||||||
|
- **错误日志**: 详细的错误信息和调试输出
|
||||||
|
- **状态监控**: 实时网关和连接状态
|
||||||
|
|
||||||
|
## 🛠️ 开发指南
|
||||||
|
|
||||||
|
### 添加新城市
|
||||||
|
在 `config.py` 中的 `city_dict` 添加新的省份和城市:
|
||||||
|
|
||||||
|
```python
|
||||||
|
city_dict = {
|
||||||
|
"新省份": {
|
||||||
|
"城市1": "城市编码1",
|
||||||
|
"城市2": "城市编码2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义配置
|
||||||
|
修改 `app/.env` 文件中的配置项,或通过环境变量覆盖。
|
||||||
|
|
||||||
|
### 扩展功能
|
||||||
|
- 在 `rotation_service.py` 中添加新的轮换策略
|
||||||
|
- 在 `eip_client.py` 中添加新的EIP操作
|
||||||
|
- 在 `routers/proxy.py` 中添加新的API端点
|
||||||
|
|
||||||
|
## 📝 更新日志
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- ✅ 基础IP轮换功能
|
||||||
|
- ✅ 多省份支持
|
||||||
|
- ✅ Redis存储集成
|
||||||
|
- ✅ Web控制面板
|
||||||
|
- ✅ 自动重试机制
|
||||||
|
- ✅ 详细中文注释
|
||||||
|
|
||||||
|
## 🤝 贡献指南
|
||||||
|
|
||||||
|
1. Fork 项目
|
||||||
|
2. 创建功能分支
|
||||||
|
3. 提交更改
|
||||||
|
4. 推送到分支
|
||||||
|
5. 创建 Pull Request
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
本项目采用 MIT 许可证。
|
||||||
|
|
||||||
|
## 🆘 支持
|
||||||
|
|
||||||
|
如有问题或建议,请创建 Issue 或联系开发团队。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**注意**: 使用前请确保已正确配置EIP服务凭据和Redis连接。
|
||||||
|
|||||||
17
app/main.py
17
app/main.py
@ -1,15 +1,30 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .routers.proxy import router as proxy_router
|
from .routers.proxy import router as proxy_router
|
||||||
|
|
||||||
app = FastAPI(title="EIP Rotation Service")
|
app = FastAPI(title="EIP Rotation Service")
|
||||||
|
|
||||||
|
# 设置模板目录
|
||||||
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health_check():
|
def health_check():
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request, id: str = None):
|
||||||
|
"""主页 - IP轮换控制面板"""
|
||||||
|
return templates.TemplateResponse("index.html", {
|
||||||
|
"request": request,
|
||||||
|
"client_id": id or "1" # 默认ID为1
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
app.include_router(proxy_router, prefix="/proxy", tags=["proxy"])
|
app.include_router(proxy_router, prefix="/proxy", tags=["proxy"])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
362
app/templates/index.html
Normal file
362
app/templates/index.html
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>EIP IP轮换控制面板</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1.1em;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
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));
|
||||||
|
gap: 15px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item input[type="checkbox"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.success {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
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 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🌐 EIP IP轮换控制面板</h1>
|
||||||
|
<p>智能IP地址轮换管理系统</p>
|
||||||
|
</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>
|
||||||
|
<div class="select-all-container">
|
||||||
|
<input type="checkbox" id="select-all" checked>
|
||||||
|
<label for="select-all">全选/取消全选</label>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox-container" id="provinces-container">
|
||||||
|
<!-- 省份选项将通过JavaScript动态加载 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="btn-container">
|
||||||
|
<button class="btn" id="rotate-btn" onclick="performRotate()">
|
||||||
|
🔄 开始IP轮换
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在执行IP轮换,请稍候...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 结果显示 -->
|
||||||
|
<div class="result" id="result"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let allProvinces = [];
|
||||||
|
let selectedProvinces = [];
|
||||||
|
|
||||||
|
// 页面加载时初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadProvinces();
|
||||||
|
setupSelectAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载省份列表
|
||||||
|
async function loadProvinces() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/proxy/cities');
|
||||||
|
const data = await response.json();
|
||||||
|
allProvinces = data.data;
|
||||||
|
selectedProvinces = [...allProvinces]; // 默认全选
|
||||||
|
renderProvinces();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载省份失败:', error);
|
||||||
|
showResult('加载省份列表失败: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染省份选项
|
||||||
|
function renderProvinces() {
|
||||||
|
const container = document.getElementById('provinces-container');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
allProvinces.forEach(province => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'checkbox-item';
|
||||||
|
div.innerHTML = `
|
||||||
|
<input type="checkbox" id="province-${province}" value="${province}"
|
||||||
|
${selectedProvinces.includes(province) ? 'checked' : ''}
|
||||||
|
onchange="updateSelection()">
|
||||||
|
<label for="province-${province}">${province}</label>
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置全选功能
|
||||||
|
function setupSelectAll() {
|
||||||
|
const selectAllCheckbox = document.getElementById('select-all');
|
||||||
|
selectAllCheckbox.addEventListener('change', function() {
|
||||||
|
const checkboxes = document.querySelectorAll('#provinces-container input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = this.checked;
|
||||||
|
});
|
||||||
|
updateSelection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新选择状态
|
||||||
|
function updateSelection() {
|
||||||
|
const checkboxes = document.querySelectorAll('#provinces-container input[type="checkbox"]');
|
||||||
|
selectedProvinces = Array.from(checkboxes)
|
||||||
|
.filter(cb => cb.checked)
|
||||||
|
.map(cb => cb.value);
|
||||||
|
|
||||||
|
// 更新全选状态
|
||||||
|
const selectAllCheckbox = document.getElementById('select-all');
|
||||||
|
selectAllCheckbox.checked = selectedProvinces.length === allProvinces.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行IP轮换
|
||||||
|
async function performRotate() {
|
||||||
|
if (selectedProvinces.length === 0) {
|
||||||
|
showResult('请至少选择一个省份!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = document.getElementById('client-id').textContent;
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
document.getElementById('loading').style.display = 'block';
|
||||||
|
document.getElementById('rotate-btn').disabled = true;
|
||||||
|
document.getElementById('result').style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/proxy/rotate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: parseInt(clientId),
|
||||||
|
citys: selectedProvinces.join(',')
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.changed) {
|
||||||
|
showResult(`
|
||||||
|
<h3>✅ IP轮换成功!</h3>
|
||||||
|
<p><strong>新IP地址:</strong> ${result.ip}</p>
|
||||||
|
<p><strong>边缘设备:</strong> ${result.edge}</p>
|
||||||
|
<p><strong>网关状态:</strong> 已更新</p>
|
||||||
|
`, 'success');
|
||||||
|
} else {
|
||||||
|
showResult(`
|
||||||
|
<h3>⚠️ IP轮换失败</h3>
|
||||||
|
<p><strong>原因:</strong> ${result.reason}</p>
|
||||||
|
`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('轮换失败:', error);
|
||||||
|
showResult('IP轮换失败: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
// 隐藏加载状态
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
document.getElementById('rotate-btn').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示结果
|
||||||
|
function showResult(message, type) {
|
||||||
|
const resultDiv = document.getElementById('result');
|
||||||
|
resultDiv.innerHTML = message;
|
||||||
|
resultDiv.className = `result ${type}`;
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -5,4 +5,5 @@ pydantic==2.9.2
|
|||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
redis==5.0.8
|
redis==5.0.8
|
||||||
tenacity==9.0.0
|
tenacity==9.0.0
|
||||||
|
jinja2==3.1.4
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user