加了前端

This commit is contained in:
wangqifan 2025-10-21 20:04:19 +08:00
parent 102ac41025
commit 5d61a240ab
4 changed files with 614 additions and 53 deletions

287
README.md
View File

@ -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连接。

View File

@ -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
View 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>

View File

@ -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