commit
10
.env.example
@@ -24,3 +24,13 @@ LOG_DIR=./logs
|
||||
|
||||
# 调试模式
|
||||
DEBUG=True
|
||||
|
||||
# 测试配置
|
||||
AUTO_CLOSE_BROWSER=False
|
||||
|
||||
# MySQL数据库配置
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=your_password
|
||||
MYSQL_DATABASE=ai_article
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# 虚拟环境使用指南
|
||||
|
||||
## 快速开始(推荐)
|
||||
|
||||
直接双击运行 `start.bat`,脚本会自动:
|
||||
1. 检查并创建虚拟环境
|
||||
2. 激活虚拟环境
|
||||
3. 安装依赖包
|
||||
4. 启动服务
|
||||
|
||||
## 手动使用虚拟环境
|
||||
|
||||
### Windows 系统
|
||||
|
||||
```bash
|
||||
# 1. 创建虚拟环境
|
||||
python -m venv venv
|
||||
|
||||
# 2. 激活虚拟环境
|
||||
venv\Scripts\activate
|
||||
|
||||
# 3. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 4. 运行服务
|
||||
python app.py
|
||||
|
||||
# 5. 退出虚拟环境(使用完毕后)
|
||||
deactivate
|
||||
```
|
||||
|
||||
### Linux/Mac 系统
|
||||
|
||||
```bash
|
||||
# 1. 创建虚拟环境
|
||||
python3 -m venv venv
|
||||
|
||||
# 2. 激活虚拟环境
|
||||
source venv/bin/activate
|
||||
|
||||
# 3. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 4. 运行服务
|
||||
python app.py
|
||||
|
||||
# 5. 退出虚拟环境
|
||||
deactivate
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
在启动服务前,需要配置 `.env` 文件:
|
||||
|
||||
**必须配置项:**
|
||||
- `ADSPOWER_USER_ID`: 在 AdsPower 中创建的用户 ID
|
||||
|
||||
**可选配置项:**
|
||||
- 其他配置项保持默认值即可
|
||||
|
||||
## 验证虚拟环境
|
||||
|
||||
激活虚拟环境后,命令行提示符前会显示 `(venv)`:
|
||||
|
||||
```
|
||||
(venv) D:\project\Work\ai_mip>
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 如何确认已在虚拟环境中?**
|
||||
A: 命令行提示符前有 `(venv)` 标识
|
||||
|
||||
**Q: 虚拟环境在哪里?**
|
||||
A: 项目根目录下的 `venv` 文件夹
|
||||
|
||||
**Q: 需要每次都激活虚拟环境吗?**
|
||||
A: 使用 `start.bat` 会自动激活,手动运行需要先激活
|
||||
|
||||
**Q: 如何重建虚拟环境?**
|
||||
A: 删除 `venv` 文件夹,重新运行 `start.bat` 或手动创建
|
||||
395
ad_automation.py
@@ -4,28 +4,78 @@ from typing import Optional, Tuple
|
||||
from playwright.sync_api import Page, ElementHandle
|
||||
from loguru import logger
|
||||
from config import Config
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class MIPAdAutomation:
|
||||
"""MIP页面广告自动化操作"""
|
||||
|
||||
def __init__(self, page: Page):
|
||||
# 预设的咨询语句
|
||||
CONSULTATION_MESSAGES = [
|
||||
"我想要预约一个医生,有什么推荐吗?",
|
||||
"我现在本人不在当地,医生什么时候有空,是随时能去吗?有没有推荐的医生。",
|
||||
"咱们医院是周六日是否上班,随时去吗?",
|
||||
"想找医生看看,有没有推荐的区生",
|
||||
"最近很不舒服,也说不出来全部的症状,能不能直接对话医生?"
|
||||
]
|
||||
|
||||
def __init__(self, page: Page, task_index: int = None):
|
||||
self.page = page
|
||||
self.site_id = None # 当前站点ID
|
||||
self.click_id = None # 当前点击ID
|
||||
self.task_folder = None # 任务日志目录
|
||||
|
||||
def check_and_click_ad(self, url: str) -> Tuple[bool, bool]:
|
||||
# 创建任务日志目录
|
||||
if task_index:
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
self.task_folder = Path("./test") / f"task_{task_index}_{timestamp}"
|
||||
self.task_folder.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"任务日志目录: {self.task_folder}")
|
||||
|
||||
def check_and_click_ad(self, url: str, site_id: int = None) -> Tuple[bool, bool]:
|
||||
"""
|
||||
检查并点击广告
|
||||
|
||||
Args:
|
||||
url: MIP页面链接
|
||||
site_id: 站点ID(用于数据库记录)
|
||||
|
||||
Returns:
|
||||
(是否点击成功, 是否获得回复)
|
||||
"""
|
||||
self.site_id = site_id
|
||||
|
||||
try:
|
||||
# 访问链接
|
||||
logger.info(f"访问链接: {url}")
|
||||
self.page.goto(url, wait_until='domcontentloaded')
|
||||
# 访问链接(带重试机制)
|
||||
max_retries = 2
|
||||
page_loaded = False
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
logger.info(f"访问链接: {url} (第{attempt+1}次尝试)")
|
||||
self.page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
||||
page_loaded = True
|
||||
break
|
||||
except Exception as goto_err:
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning(f"访问超时,尝试刷新页面...")
|
||||
try:
|
||||
self.page.reload(wait_until='domcontentloaded', timeout=30000)
|
||||
logger.info("✅ 页面刷新成功")
|
||||
page_loaded = True
|
||||
break
|
||||
except:
|
||||
logger.warning(f"刷新失败,等待2秒后重试...")
|
||||
time.sleep(2)
|
||||
else:
|
||||
logger.error(f"访问链接失败: {str(goto_err)}")
|
||||
# 记录访问失败
|
||||
self._record_click_failure(url, f"访问超时: {str(goto_err)}")
|
||||
return False, False
|
||||
|
||||
if not page_loaded:
|
||||
self._record_click_failure(url, "页面加载失败")
|
||||
return False, False
|
||||
|
||||
# 等待页面加载
|
||||
time.sleep(3)
|
||||
@@ -43,9 +93,19 @@ class MIPAdAutomation:
|
||||
logger.warning("点击广告失败")
|
||||
return False, False
|
||||
|
||||
# 记录点击到数据库
|
||||
self._record_click(url)
|
||||
|
||||
# 发送咨询消息
|
||||
message_sent = self._send_consultation_message()
|
||||
|
||||
# 等待并检查回复
|
||||
has_reply = self._wait_for_reply()
|
||||
|
||||
# 记录互动到数据库
|
||||
if message_sent:
|
||||
self._record_interaction(has_reply)
|
||||
|
||||
return True, has_reply
|
||||
|
||||
except Exception as e:
|
||||
@@ -99,7 +159,7 @@ class MIPAdAutomation:
|
||||
|
||||
def _click_advertisement(self, ad_element: ElementHandle) -> bool:
|
||||
"""
|
||||
点击广告元素
|
||||
点击广告元素(当前页面导航)
|
||||
|
||||
Args:
|
||||
ad_element: 广告元素
|
||||
@@ -108,26 +168,35 @@ class MIPAdAutomation:
|
||||
是否点击成功
|
||||
"""
|
||||
try:
|
||||
# 获取当前页面
|
||||
context = self.page.context
|
||||
original_url = self.page.url
|
||||
|
||||
# 滚动到广告元素可见
|
||||
ad_element.scroll_into_view_if_needed()
|
||||
time.sleep(1)
|
||||
|
||||
# 监听新页面打开
|
||||
with context.expect_page() as new_page_info:
|
||||
# 点击广告
|
||||
ad_element.click()
|
||||
logger.info("已点击广告")
|
||||
# 直接点击广告(当前页面导航)
|
||||
logger.info("点击广告...")
|
||||
ad_element.click()
|
||||
logger.info("已点击广告")
|
||||
|
||||
# 等待新页面
|
||||
new_page = new_page_info.value
|
||||
new_page.wait_for_load_state('domcontentloaded')
|
||||
# 等待页面导航(增加等待时间,支持慢速电脑)
|
||||
logger.info("等待页面跳转...")
|
||||
max_wait = 10 # 最多等待10秒
|
||||
check_interval = 1 # 每秒检查一次
|
||||
|
||||
# 切换到新页面
|
||||
self.page = new_page
|
||||
logger.info("已切换到广告页面")
|
||||
for i in range(max_wait):
|
||||
time.sleep(check_interval)
|
||||
if self.page.url != original_url:
|
||||
logger.info(f"✅ 页面已导航(耗时{i+1}秒): {original_url} -> {self.page.url}")
|
||||
self.page.wait_for_load_state('domcontentloaded')
|
||||
break
|
||||
else:
|
||||
# 循环正常结束(未跳转)
|
||||
logger.error(f"❌ 页面URL未变化(等待{max_wait}秒后),广告点击失败: {self.page.url}")
|
||||
return False
|
||||
|
||||
# 等待聊天页面加载
|
||||
time.sleep(2)
|
||||
|
||||
return True
|
||||
|
||||
@@ -135,6 +204,294 @@ class MIPAdAutomation:
|
||||
logger.error(f"点击广告异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def _send_consultation_message(self) -> bool:
|
||||
"""
|
||||
在聊天页面发送随机咨询消息
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
try:
|
||||
logger.info("准备发送咨询消息...")
|
||||
|
||||
# 随机选择一条消息
|
||||
message = random.choice(self.CONSULTATION_MESSAGES)
|
||||
logger.info(f"选择的消息: {message}")
|
||||
|
||||
# 等待页面加载完成
|
||||
time.sleep(2)
|
||||
|
||||
# 打印当前页面URL
|
||||
logger.info(f"当前页面: {self.page.url}")
|
||||
|
||||
# 常见的输入框选择器(优先通过placeholder查找)
|
||||
input_selectors = [
|
||||
# 优先:通过placeholder查找
|
||||
"textarea[placeholder*='消息']",
|
||||
"textarea[placeholder*='问题']",
|
||||
"input[type='text'][placeholder*='消息']",
|
||||
"input[type='text'][placeholder*='问题']",
|
||||
"textarea[placeholder*='输入']",
|
||||
"textarea[placeholder*='发送']",
|
||||
"input[type='text'][placeholder*='输入']",
|
||||
"input[type='text'][placeholder*='发送']",
|
||||
# 次选:通过class查找
|
||||
"textarea[class*='input']",
|
||||
# 兜底:通用选择器
|
||||
"div[contenteditable='true']",
|
||||
"textarea",
|
||||
"input[type='text']"
|
||||
]
|
||||
|
||||
input_element = None
|
||||
logger.info("开始查找输入框...")
|
||||
for selector in input_selectors:
|
||||
try:
|
||||
elements = self.page.locator(selector).all()
|
||||
logger.debug(f"选择器 {selector} 找到 {len(elements)} 个元素")
|
||||
for elem in elements:
|
||||
if elem.is_visible():
|
||||
input_element = elem
|
||||
logger.info(f"✅ 找到可见输入框: {selector}")
|
||||
break
|
||||
if input_element:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"选择器 {selector} 失败: {str(e)}")
|
||||
continue
|
||||
|
||||
if not input_element:
|
||||
logger.warning("❌ 未找到输入框")
|
||||
# 尝试截图便于调试
|
||||
try:
|
||||
if self.task_folder:
|
||||
screenshot_path = self.task_folder / "debug_no_input.png"
|
||||
else:
|
||||
screenshot_path = Path(f"./logs/debug_no_input_{int(time.time())}.png")
|
||||
self.page.screenshot(path=str(screenshot_path))
|
||||
logger.info(f"已保存调试截图: {screenshot_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"截图失败: {str(e)}")
|
||||
|
||||
# 兜底方案:尝试查找并点击任何可能的输入区域
|
||||
logger.warning("尝试兜底方案:查找所有可能的输入区域...")
|
||||
try:
|
||||
# 先滚动到页面最底部
|
||||
self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
time.sleep(1)
|
||||
|
||||
# 尝试查找所有可能的输入相关元素并点击
|
||||
fallback_selectors = [
|
||||
"textarea",
|
||||
"input[type='text']",
|
||||
"div[contenteditable='true']",
|
||||
"div[class*='input']",
|
||||
"div[class*='textarea']",
|
||||
"div[class*='message']",
|
||||
"div[class*='chat']",
|
||||
"div[id*='input']",
|
||||
"div[id*='message']"
|
||||
]
|
||||
|
||||
clicked = False
|
||||
for selector in fallback_selectors:
|
||||
try:
|
||||
elements = self.page.locator(selector).all()
|
||||
logger.debug(f"兜底选择器 {selector} 找到 {len(elements)} 个元素")
|
||||
for elem in elements:
|
||||
if elem.is_visible():
|
||||
# 滚动到元素位置
|
||||
elem.scroll_into_view_if_needed()
|
||||
time.sleep(0.5)
|
||||
# 点击元素
|
||||
elem.click()
|
||||
time.sleep(1)
|
||||
logger.info(f"已点击元素: {selector}")
|
||||
clicked = True
|
||||
break
|
||||
if clicked:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"兜底选择器 {selector} 失败: {str(e)}")
|
||||
continue
|
||||
|
||||
if clicked:
|
||||
# 直接输入消息
|
||||
self.page.keyboard.type(message, delay=50)
|
||||
logger.info("✅ 已输入消息(兜底)")
|
||||
|
||||
# 直接按回车发送
|
||||
self.page.keyboard.press('Enter')
|
||||
logger.info("✅ 已按回车键发送(兜底)")
|
||||
|
||||
# 保存已发送的消息内容
|
||||
self.sent_message = message
|
||||
time.sleep(2)
|
||||
return True
|
||||
else:
|
||||
logger.error("❌ 兜底方案未找到任何可点击的输入区域")
|
||||
return False
|
||||
|
||||
except Exception as fallback_err:
|
||||
logger.error(f"兜底方案失败: {str(fallback_err)}")
|
||||
return False
|
||||
|
||||
# 正常流程:点击输入框获取焦点
|
||||
input_element.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# 输入消息
|
||||
input_element.fill(message)
|
||||
logger.info("✅ 已输入消息")
|
||||
time.sleep(1)
|
||||
|
||||
# 尝试发送消息(优先回车,再尝试按钮)
|
||||
sent = False
|
||||
|
||||
# 方法1(优先):按回车键发送
|
||||
try:
|
||||
logger.info("尝试按回车键发送...")
|
||||
input_element.press('Enter')
|
||||
logger.info("✅ 已按回车键发送")
|
||||
sent = True
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
logger.warning(f"❌ 按回车键失败: {str(e)}")
|
||||
|
||||
# 方法2(兜底): 尝试找到发送按钮并点击
|
||||
if not sent:
|
||||
send_button_selectors = [
|
||||
"button:has-text('发送')",
|
||||
"button[class*='send']",
|
||||
"button[type='submit']",
|
||||
"div[class*='send']",
|
||||
"span:has-text('发送')"
|
||||
]
|
||||
|
||||
logger.info("开始查找发送按钮...")
|
||||
for selector in send_button_selectors:
|
||||
try:
|
||||
buttons = self.page.locator(selector).all()
|
||||
logger.debug(f"选择器 {selector} 找到 {len(buttons)} 个按钮")
|
||||
for btn in buttons:
|
||||
if btn.is_visible() and btn.is_enabled():
|
||||
btn.click()
|
||||
logger.info(f"✅ 已点击发送按钮: {selector}")
|
||||
sent = True
|
||||
break
|
||||
if sent:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"选择器 {selector} 失败: {str(e)}")
|
||||
continue
|
||||
|
||||
if sent:
|
||||
logger.info("✅ 消息发送成功")
|
||||
# 保存已发送的消息内容
|
||||
self.sent_message = message
|
||||
time.sleep(2) # 等待消息发送完成
|
||||
return True
|
||||
else:
|
||||
logger.warning("❌ 未能发送消息")
|
||||
# 截图调试
|
||||
try:
|
||||
if self.task_folder:
|
||||
screenshot_path = self.task_folder / "debug_send_failed.png"
|
||||
else:
|
||||
screenshot_path = Path(f"./logs/debug_send_failed_{int(time.time())}.png")
|
||||
self.page.screenshot(path=str(screenshot_path))
|
||||
logger.info(f"已保存调试截图: {screenshot_path}")
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息异常: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _record_click(self, site_url: str):
|
||||
"""记录点击到数据库"""
|
||||
try:
|
||||
if not self.site_id:
|
||||
logger.warning("未设置 site_id,跳过点击记录")
|
||||
return
|
||||
|
||||
from db_manager import ClickManager
|
||||
click_mgr = ClickManager()
|
||||
self.click_id = click_mgr.record_click(
|
||||
site_id=self.site_id,
|
||||
site_url=site_url,
|
||||
user_ip=None, # 可以后续添加代理IP
|
||||
device_type='pc'
|
||||
)
|
||||
logger.info(f"已记录点击: click_id={self.click_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"记录点击失败: {str(e)}")
|
||||
|
||||
def _record_click_failure(self, site_url: str, error_message: str):
|
||||
"""
|
||||
记录点击失败到数据库
|
||||
|
||||
Args:
|
||||
site_url: 站点URL
|
||||
error_message: 错误信息
|
||||
"""
|
||||
try:
|
||||
if not self.site_id:
|
||||
logger.warning("未设置 site_id,跳过失败记录")
|
||||
return
|
||||
|
||||
from db_manager import ClickManager
|
||||
click_mgr = ClickManager()
|
||||
# 记录点击(失败也计数)
|
||||
self.click_id = click_mgr.record_click(
|
||||
site_id=self.site_id,
|
||||
site_url=site_url,
|
||||
user_ip=None,
|
||||
device_type='pc'
|
||||
)
|
||||
|
||||
# 记录互动失败
|
||||
from db_manager import InteractionManager
|
||||
interaction_mgr = InteractionManager()
|
||||
interaction_mgr.record_interaction(
|
||||
site_id=self.site_id,
|
||||
click_id=self.click_id,
|
||||
interaction_type='reply',
|
||||
reply_content=None,
|
||||
is_successful=False,
|
||||
response_received=False,
|
||||
error_message=error_message
|
||||
)
|
||||
logger.info(f"已记录失败: {error_message}")
|
||||
except Exception as e:
|
||||
logger.error(f"记录失败异常: {str(e)}")
|
||||
|
||||
def _record_interaction(self, response_received: bool):
|
||||
"""记录互动到数据库"""
|
||||
try:
|
||||
if not self.site_id:
|
||||
logger.warning("未设置 site_id,跳过互动记录")
|
||||
return
|
||||
|
||||
from db_manager import InteractionManager
|
||||
interaction_mgr = InteractionManager()
|
||||
|
||||
interaction_id = interaction_mgr.record_interaction(
|
||||
site_id=self.site_id,
|
||||
click_id=self.click_id,
|
||||
interaction_type='message', # 符合数据库ENUM定义:reply/comment/message/form_submit/follow/like/share
|
||||
reply_content=getattr(self, 'sent_message', None),
|
||||
is_successful=True,
|
||||
response_received=response_received,
|
||||
response_content=None # 可以后续添加提取回复内容
|
||||
)
|
||||
logger.info(f"已记录互动: interaction_id={interaction_id}, response={response_received}")
|
||||
except Exception as e:
|
||||
logger.error(f"记录互动失败: {str(e)}")
|
||||
|
||||
def _wait_for_reply(self) -> bool:
|
||||
"""
|
||||
等待广告主回复
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
from loguru import logger
|
||||
from playwright.sync_api import sync_playwright, Browser, Page
|
||||
from config import Config
|
||||
import json
|
||||
|
||||
|
||||
class AdsPowerClient:
|
||||
@@ -28,6 +28,72 @@ class AdsPowerClient:
|
||||
logger.debug(f"AdsPowerClient 初始化 - API Key: {self.api_key[:8]}... (len={len(self.api_key)})")
|
||||
else:
|
||||
logger.debug("AdsPowerClient 初始化 - 未配置 API Key")
|
||||
|
||||
def _make_request(self, method: str, endpoint: str, json: Dict = None, params: Dict = None) -> Optional[Dict]:
|
||||
"""
|
||||
通用 API 请求方法
|
||||
|
||||
Args:
|
||||
method: HTTP方法 (GET/POST/PUT/DELETE)
|
||||
endpoint: API端点,如 '/api/v2/browser-profile/create'
|
||||
json: JSON请求体
|
||||
params: URL查询参数
|
||||
|
||||
Returns:
|
||||
响应JSON或None
|
||||
"""
|
||||
try:
|
||||
import json as json_lib # 避免参数名冲突
|
||||
|
||||
url = f"{self.api_url}{endpoint}"
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if self.api_key:
|
||||
headers['Authorization'] = f'Bearer {self.api_key}'
|
||||
|
||||
# 记录请求
|
||||
logger.info("\n" + "="*70)
|
||||
logger.info(f"API请求: {method} {endpoint}")
|
||||
logger.info("="*70)
|
||||
logger.info(f"URL: {url}")
|
||||
if params:
|
||||
logger.info(f"Params:\n{json_lib.dumps(params, indent=2, ensure_ascii=False)}")
|
||||
if json:
|
||||
logger.info(f"Body:\n{json_lib.dumps(json, indent=2, ensure_ascii=False)}")
|
||||
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
json=json,
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# 记录响应
|
||||
logger.info("\n" + "-"*70)
|
||||
logger.info("API响应")
|
||||
logger.info("-"*70)
|
||||
logger.info(f"Status: {response.status_code}")
|
||||
|
||||
try:
|
||||
response_data = response.json()
|
||||
logger.info(f"Body:\n{json_lib.dumps(response_data, indent=2, ensure_ascii=False)}")
|
||||
except:
|
||||
logger.info(f"Body (Raw):\n{response.text}")
|
||||
response_data = None
|
||||
|
||||
logger.info("="*70 + "\n")
|
||||
|
||||
return response_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"API请求异常 [{method} {endpoint}]: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def update_profile_proxy(self, profile_id: str, proxy_id: str) -> bool:
|
||||
"""
|
||||
@@ -553,21 +619,70 @@ class AdsPowerClient:
|
||||
Playwright Browser 实例
|
||||
"""
|
||||
try:
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
# 检测是否在 asyncio 事件循环中
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
logger.warning("检测到 asyncio 事件循环,将在新线程中执行 Playwright")
|
||||
|
||||
# 在新线程中执行 Playwright 同步 API
|
||||
import threading
|
||||
result_container = {'browser': None, 'error': None}
|
||||
|
||||
def run_playwright():
|
||||
try:
|
||||
# 获取 CDP WebSocket 端点
|
||||
ws_endpoint = browser_info['data']['ws']['puppeteer']
|
||||
|
||||
# 创建新的 Playwright 实例
|
||||
playwright = sync_playwright().start()
|
||||
|
||||
# 通过 CDP 连接到浏览器
|
||||
browser = playwright.chromium.connect_over_cdp(ws_endpoint)
|
||||
logger.info("成功通过 CDP 连接到 AdsPower 浏览器")
|
||||
|
||||
# 保存引用
|
||||
self.playwright = playwright
|
||||
self.browser = browser
|
||||
result_container['browser'] = browser
|
||||
except Exception as e:
|
||||
result_container['error'] = str(e)
|
||||
|
||||
thread = threading.Thread(target=run_playwright)
|
||||
thread.start()
|
||||
thread.join(timeout=30)
|
||||
|
||||
if result_container['error']:
|
||||
raise Exception(result_container['error'])
|
||||
|
||||
return result_container['browser']
|
||||
|
||||
except RuntimeError:
|
||||
# 没有运行中的事件循环,正常执行
|
||||
pass
|
||||
|
||||
# 获取 CDP WebSocket 端点
|
||||
ws_endpoint = browser_info['data']['ws']['puppeteer']
|
||||
|
||||
# 初始化 Playwright
|
||||
if not self.playwright:
|
||||
self.playwright = sync_playwright().start()
|
||||
# 创建新的 Playwright 实例
|
||||
playwright = sync_playwright().start()
|
||||
|
||||
# 通过 CDP 连接到浏览器
|
||||
self.browser = self.playwright.chromium.connect_over_cdp(ws_endpoint)
|
||||
browser = playwright.chromium.connect_over_cdp(ws_endpoint)
|
||||
logger.info("成功通过 CDP 连接到 AdsPower 浏览器")
|
||||
|
||||
return self.browser
|
||||
# 保存引用
|
||||
self.playwright = playwright
|
||||
self.browser = browser
|
||||
|
||||
return browser
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"CDP 连接失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def get_page(self, browser: Browser) -> Optional[Page]:
|
||||
@@ -912,7 +1027,119 @@ class AdsPowerClient:
|
||||
logger.error(f"检查浏览器状态异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def list_profiles(self, group_id: str = None, page: int = 1, page_size: int = 100) -> Optional[Dict]:
|
||||
def list_groups(self, group_name: str = None, page: int = 1, page_size: int = 2000) -> Optional[Dict]:
|
||||
"""
|
||||
查询分组列表
|
||||
使用 AdsPower API v1
|
||||
|
||||
Args:
|
||||
group_name: 分组名称(可选)
|
||||
page: 页码
|
||||
page_size: 每页数量(范围 1 ~ 2000)
|
||||
|
||||
Returns:
|
||||
分组列表信息
|
||||
"""
|
||||
try:
|
||||
url = f"{self.api_url}/api/v1/group/list"
|
||||
|
||||
# 准备请求头
|
||||
headers = {}
|
||||
if self.api_key:
|
||||
headers['Authorization'] = f'Bearer {self.api_key}'
|
||||
|
||||
# 准备请求参数
|
||||
params = {
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
if group_name:
|
||||
params["group_name"] = group_name
|
||||
|
||||
logger.info("\n" + "="*70)
|
||||
logger.info("📂 查询分组列表")
|
||||
logger.info("="*70)
|
||||
logger.info(f"URL: {url}")
|
||||
logger.info(f"Method: GET")
|
||||
logger.info(f"Params: {params}")
|
||||
|
||||
response = requests.get(url, params=params, headers=headers, timeout=30)
|
||||
|
||||
logger.info("\n" + "-"*70)
|
||||
logger.info("📥 响应信息")
|
||||
logger.info("-"*70)
|
||||
logger.info(f"Status Code: {response.status_code}")
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
logger.info(f"Response Body:")
|
||||
logger.info(json.dumps(response_json, indent=2, ensure_ascii=False))
|
||||
except:
|
||||
logger.info(f"Response Body (Raw):")
|
||||
logger.info(response.text)
|
||||
|
||||
logger.info("="*70 + "\n")
|
||||
|
||||
result = response_json if 'response_json' in locals() else response.json()
|
||||
|
||||
if result.get('code') == 0:
|
||||
groups = result.get('data', {}).get('list', [])
|
||||
logger.success(f"查询成功,找到 {len(groups)} 个分组")
|
||||
|
||||
if groups:
|
||||
logger.info("\n分组列表:")
|
||||
for idx, group in enumerate(groups, 1):
|
||||
group_id = group.get('group_id', 'N/A')
|
||||
group_name = group.get('group_name', 'N/A')
|
||||
remark = group.get('remark', '')
|
||||
logger.info(f" {idx}. ID: {group_id} | 名称: {group_name} | 备注: {remark}")
|
||||
|
||||
return result
|
||||
else:
|
||||
logger.error(f"查询分组失败: {result.get('msg')}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询分组异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_group_by_env(self) -> Optional[str]:
|
||||
"""
|
||||
根据当前运行环境获取对应的分组ID
|
||||
dev环境查询 group_name=dev
|
||||
生产环境查询 group_name=prod
|
||||
|
||||
Returns:
|
||||
分组ID,失败返回None
|
||||
"""
|
||||
# 根据环境决定分组名
|
||||
group_name = 'dev' if Config.ENV == 'development' else 'prod'
|
||||
|
||||
logger.info(f"当前运行环境: {Config.ENV},查询分组: {group_name}")
|
||||
|
||||
# 等待一下避免请求过于频繁
|
||||
import time
|
||||
time.sleep(1.0)
|
||||
|
||||
# 查询分组
|
||||
result = self.list_groups(group_name=group_name)
|
||||
|
||||
if result and result.get('code') == 0:
|
||||
groups = result.get('data', {}).get('list', [])
|
||||
if groups:
|
||||
group_id = groups[0].get('group_id')
|
||||
logger.success(f"获取到分组ID: {group_id}")
|
||||
return group_id
|
||||
else:
|
||||
logger.warning(f"未找到名为 '{group_name}' 的分组")
|
||||
return None
|
||||
else:
|
||||
logger.error(f"查询分组失败")
|
||||
return None
|
||||
|
||||
def list_profiles(self, group_id: str = None, page: int = 1, page_size: int = 100,
|
||||
profile_id: list = None, profile_no: list = None,
|
||||
limit: int = None, sort_type: str = None, sort_order: str = None) -> Optional[Dict]:
|
||||
"""
|
||||
查询 Profile 列表
|
||||
使用 AdsPower API v2
|
||||
@@ -920,7 +1147,12 @@ class AdsPowerClient:
|
||||
Args:
|
||||
group_id: 组ID(可选)
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
page_size: 每页数量(当limit未指定时使用)
|
||||
profile_id: 环境ID数组(可选)
|
||||
profile_no: 环境编号数组(可选)
|
||||
limit: 每页大小(范围 1 ~ 200)
|
||||
sort_type: 排序类型 (profile_no/last_open_time/created_time)
|
||||
sort_order: 排序顺序 (asc/desc)
|
||||
|
||||
Returns:
|
||||
Profile 列表信息
|
||||
@@ -938,10 +1170,18 @@ class AdsPowerClient:
|
||||
# 准备请求体
|
||||
payload = {
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
"page_size": limit if limit else page_size
|
||||
}
|
||||
if group_id:
|
||||
payload["group_id"] = group_id
|
||||
if profile_id:
|
||||
payload["profile_id"] = profile_id
|
||||
if profile_no:
|
||||
payload["profile_no"] = profile_no
|
||||
if sort_type:
|
||||
payload["sort_type"] = sort_type
|
||||
if sort_order:
|
||||
payload["sort_order"] = sort_order
|
||||
|
||||
# 打印请求信息
|
||||
logger.info("\n" + "="*70)
|
||||
@@ -1018,6 +1258,35 @@ class AdsPowerClient:
|
||||
logger.error(f"查询 Profile 异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def delete_profile(self, profile_id: str) -> bool:
|
||||
"""
|
||||
删除 Profile
|
||||
使用 AdsPower API v2
|
||||
|
||||
Args:
|
||||
profile_id: Profile ID
|
||||
|
||||
Returns:
|
||||
是否成功删除
|
||||
"""
|
||||
try:
|
||||
result = self._make_request(
|
||||
'POST',
|
||||
'/api/v2/browser-profile/delete',
|
||||
json={"profile_id": [profile_id]}
|
||||
)
|
||||
|
||||
if result and result.get('code') == 0:
|
||||
logger.success(f"✅ 成功删除 Profile: {profile_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"删除 Profile 失败: {result.get('msg') if result else '请求失败'}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除 Profile 异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数,确保资源清理"""
|
||||
try:
|
||||
|
||||
BIN
ai_mip.zip
Normal file
168
app.py
@@ -33,8 +33,8 @@ scheduler = ClickScheduler()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""首页 - 重定向到数据概览"""
|
||||
return redirect('/dashboard.html')
|
||||
"""首页 - 重定向到新的单页应用"""
|
||||
return redirect('/app.html')
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
@@ -196,14 +196,103 @@ def get_scheduler_status():
|
||||
|
||||
|
||||
# AdsPower 接口调试
|
||||
@app.route('/api/adspower/profiles', methods=['GET'])
|
||||
def adspower_list_profiles():
|
||||
"""查询Profile列表"""
|
||||
@app.route('/api/adspower/groups', methods=['GET'])
|
||||
def adspower_list_groups():
|
||||
"""查询分组列表"""
|
||||
try:
|
||||
from adspower_client import AdsPowerClient
|
||||
|
||||
group_name = request.args.get('group_name')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = request.args.get('page_size', 2000, type=int)
|
||||
|
||||
client = AdsPowerClient()
|
||||
result = client.list_profiles()
|
||||
return jsonify({'success': True, 'data': result})
|
||||
result = client.list_groups(group_name=group_name, page=page, page_size=page_size)
|
||||
|
||||
if result:
|
||||
return jsonify({'success': True, 'data': result})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': '查询分组列表失败'}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"AdsPower查询分组异常: {str(e)}")
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/adspower/group/env', methods=['GET'])
|
||||
def adspower_get_group_by_env():
|
||||
"""根据当前运行环境获取对应的分组ID"""
|
||||
try:
|
||||
from adspower_client import AdsPowerClient
|
||||
|
||||
client = AdsPowerClient()
|
||||
group_id = client.get_group_by_env()
|
||||
|
||||
if group_id:
|
||||
return jsonify({'success': True, 'data': {'group_id': group_id, 'env': Config.ENV}})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': f'未找到对应环境的分组'}), 404
|
||||
except Exception as e:
|
||||
logger.error(f"AdsPower获取环境分组异常: {str(e)}")
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/adspower/profiles', methods=['GET'])
|
||||
def adspower_list_profiles():
|
||||
"""查询Profile列表(支持多个查询参数)"""
|
||||
try:
|
||||
from adspower_client import AdsPowerClient
|
||||
import json as json_module
|
||||
|
||||
# 获取查询参数
|
||||
group_id = request.args.get('group_id')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('limit', type=int) # 可选
|
||||
page_size = request.args.get('page_size', type=int) # 可选
|
||||
|
||||
# 数组参数(JSON格式)
|
||||
profile_id = request.args.get('profile_id')
|
||||
profile_no = request.args.get('profile_no')
|
||||
|
||||
# 解析JSON数组
|
||||
if profile_id:
|
||||
try:
|
||||
profile_id = json_module.loads(profile_id)
|
||||
except:
|
||||
profile_id = None
|
||||
|
||||
if profile_no:
|
||||
try:
|
||||
profile_no = json_module.loads(profile_no)
|
||||
except:
|
||||
profile_no = None
|
||||
|
||||
# 排序参数
|
||||
sort_type = request.args.get('sort_type')
|
||||
sort_order = request.args.get('sort_order')
|
||||
|
||||
# 如果没有指定group_id,尝试根据环境自动获取
|
||||
client = AdsPowerClient()
|
||||
if not group_id:
|
||||
group_id = client.get_group_by_env()
|
||||
if group_id:
|
||||
logger.info(f"自动获取到分组ID: {group_id}")
|
||||
|
||||
# 查询Profile列表
|
||||
result = client.list_profiles(
|
||||
group_id=group_id,
|
||||
page=page,
|
||||
page_size=page_size if page_size else 100,
|
||||
profile_id=profile_id,
|
||||
profile_no=profile_no,
|
||||
limit=limit,
|
||||
sort_type=sort_type,
|
||||
sort_order=sort_order
|
||||
)
|
||||
|
||||
if result:
|
||||
return jsonify({'success': True, 'data': result})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': '查询Profile列表失败'}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"AdsPower查询Profile异常: {str(e)}")
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
@@ -338,6 +427,71 @@ def adspower_update_profile_v1():
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# 数据库查询接口
|
||||
@app.route('/api/clicks', methods=['GET'])
|
||||
def get_all_clicks():
|
||||
"""获取所有点击记录"""
|
||||
try:
|
||||
from db_manager import ClickManager
|
||||
|
||||
site_id = request.args.get('site_id', type=int)
|
||||
limit = request.args.get('limit', 100, type=int)
|
||||
|
||||
click_mgr = ClickManager()
|
||||
|
||||
if site_id:
|
||||
clicks = click_mgr.get_clicks_by_site(site_id, limit=limit)
|
||||
else:
|
||||
# 获取所有点击记录
|
||||
conn = click_mgr.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"""
|
||||
SELECT * FROM ai_mip_click
|
||||
ORDER BY click_time DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
clicks = [click_mgr._dict_from_row(row) for row in rows]
|
||||
|
||||
return jsonify({'success': True, 'data': clicks})
|
||||
except Exception as e:
|
||||
logger.error(f"查询点击记录异常: {str(e)}")
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/interactions', methods=['GET'])
|
||||
def get_all_interactions():
|
||||
"""获取所有互动记录"""
|
||||
try:
|
||||
from db_manager import InteractionManager
|
||||
|
||||
site_id = request.args.get('site_id', type=int)
|
||||
limit = request.args.get('limit', 100, type=int)
|
||||
|
||||
interaction_mgr = InteractionManager()
|
||||
|
||||
if site_id:
|
||||
interactions = interaction_mgr.get_interactions_by_site(site_id, limit=limit)
|
||||
else:
|
||||
# 获取所有互动记录
|
||||
conn = interaction_mgr.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"""
|
||||
SELECT * FROM ai_mip_interaction
|
||||
ORDER BY interaction_time DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
interactions = [interaction_mgr._dict_from_row(row) for row in rows]
|
||||
|
||||
return jsonify({'success': True, 'data': interactions})
|
||||
except Exception as e:
|
||||
logger.error(f"查询互动记录异常: {str(e)}")
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info(f"启动MIP广告点击服务 - 环境: {Config.ENV}")
|
||||
logger.info(f"服务地址: http://{Config.SERVER_HOST}:{Config.SERVER_PORT}")
|
||||
|
||||
62
batch_insert_urls.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""批量插入URL到数据库"""
|
||||
from data_manager import DataManager
|
||||
|
||||
# 要插入的URL列表
|
||||
urls = [
|
||||
"https://health.baidu.com/m/detail/ar_1763832104063502612",
|
||||
"https://health.baidu.com/m/detail/ar_3234161746463547514",
|
||||
"https://health.baidu.com/m/detail/ar_2979413891570169996",
|
||||
"https://health.baidu.com/m/detail/ar_2956015846029041423",
|
||||
"https://health.baidu.com/m/detail/ar_168792171069657865",
|
||||
"https://health.baidu.com/m/detail/ar_6465728881863076989",
|
||||
"https://health.baidu.com/m/detail/ar_5239302258777444788",
|
||||
"https://health.baidu.com/m/detail/ar_4713935339392349406",
|
||||
"https://health.baidu.com/m/detail/ar_5279303492380349045",
|
||||
"https://health.baidu.com/m/detail/ar_3049436766450657685",
|
||||
"https://health.baidu.com/m/detail/ar_2014490668952387433",
|
||||
]
|
||||
|
||||
print("=" * 60)
|
||||
print("批量插入URL到数据库")
|
||||
print("=" * 60)
|
||||
|
||||
# 创建数据管理器
|
||||
dm = DataManager()
|
||||
print(f"\n存储方式: {'SQLite数据库' if dm.use_database else 'JSON文件'}")
|
||||
print(f"总URL数: {len(urls)}\n")
|
||||
|
||||
# 批量插入
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for idx, url in enumerate(urls, 1):
|
||||
print(f"[{idx}/{len(urls)}] 插入: {url}")
|
||||
result = dm.add_url(url)
|
||||
|
||||
if result:
|
||||
success_count += 1
|
||||
print(f" ✓ 成功")
|
||||
else:
|
||||
failed_count += 1
|
||||
print(f" × 失败(可能已存在)")
|
||||
|
||||
# 统计结果
|
||||
print("\n" + "=" * 60)
|
||||
print("插入完成")
|
||||
print("=" * 60)
|
||||
print(f"成功: {success_count} 个")
|
||||
print(f"失败: {failed_count} 个")
|
||||
|
||||
# 显示当前数据库统计
|
||||
print("\n数据库统计:")
|
||||
stats = dm.get_statistics()
|
||||
for key, value in stats.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# 显示所有活跃URL
|
||||
print("\n活跃URL列表:")
|
||||
active_urls = dm.get_active_urls()
|
||||
for idx, site in enumerate(active_urls[:15], 1):
|
||||
site_url = site.get('site_url', site.get('url'))
|
||||
click_count = site.get('click_count', 0)
|
||||
print(f" {idx}. {site_url} (点击: {click_count}次)")
|
||||
10
config.py
@@ -57,6 +57,16 @@ class BaseConfig:
|
||||
# 调试模式
|
||||
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
|
||||
|
||||
# 测试配置
|
||||
AUTO_CLOSE_BROWSER = os.getenv('AUTO_CLOSE_BROWSER', 'False').lower() == 'true' # 测试完成后是否自动关闭浏览器
|
||||
|
||||
# MySQL数据库配置
|
||||
MYSQL_HOST = os.getenv('MYSQL_HOST', 'localhost')
|
||||
MYSQL_PORT = int(os.getenv('MYSQL_PORT', 3306))
|
||||
MYSQL_USER = os.getenv('MYSQL_USER', 'root')
|
||||
MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD', '')
|
||||
MYSQL_DATABASE = os.getenv('MYSQL_DATABASE', 'ai_article')
|
||||
|
||||
@classmethod
|
||||
def ensure_dirs(cls):
|
||||
"""确保必要的目录存在"""
|
||||
|
||||
304
data_manager.py
@@ -1,280 +1,84 @@
|
||||
import json
|
||||
import random
|
||||
"""数据管理器 - MySQL数据库存储"""
|
||||
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
from config import Config
|
||||
|
||||
# 导入MySQL数据库管理器
|
||||
try:
|
||||
from db_manager import SiteManager, ClickManager, InteractionManager, StatisticsManager
|
||||
logger.info("使用MySQL数据库存储")
|
||||
except Exception as e:
|
||||
logger.error(f"MySQL数据库初始化失败: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
class DataManager:
|
||||
"""数据管理器,负责URL和统计数据的存储与管理"""
|
||||
"""数据管理器 - MySQL数据库存储"""
|
||||
|
||||
def __init__(self, data_file: str = None):
|
||||
self.data_file = data_file or str(Path(Config.DATA_DIR) / 'urls_data.json')
|
||||
self._ensure_data_file()
|
||||
self.data = self._load_data()
|
||||
|
||||
def _ensure_data_file(self):
|
||||
"""确保数据文件存在"""
|
||||
Config.ensure_dirs()
|
||||
if not Path(self.data_file).exists():
|
||||
self._save_data({'urls': {}})
|
||||
|
||||
def _load_data(self) -> Dict:
|
||||
"""加载数据"""
|
||||
try:
|
||||
with open(self.data_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"加载数据文件失败: {str(e)}")
|
||||
return {'urls': {}}
|
||||
|
||||
def _save_data(self, data: Dict = None):
|
||||
"""保存数据"""
|
||||
try:
|
||||
save_data = data if data is not None else self.data
|
||||
with open(self.data_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(save_data, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
logger.error(f"保存数据文件失败: {str(e)}")
|
||||
"""初始化MySQL数据库管理器"""
|
||||
self.site_mgr = SiteManager()
|
||||
self.click_mgr = ClickManager()
|
||||
self.interaction_mgr = InteractionManager()
|
||||
self.stats_mgr = StatisticsManager()
|
||||
logger.info("初始化MySQL数据库管理器")
|
||||
|
||||
|
||||
def add_url(self, url: str) -> bool:
|
||||
"""
|
||||
添加新URL
|
||||
|
||||
Args:
|
||||
url: MIP页面链接
|
||||
|
||||
Returns:
|
||||
是否添加成功
|
||||
"""
|
||||
try:
|
||||
if url in self.data['urls']:
|
||||
logger.warning(f"URL已存在: {url}")
|
||||
return False
|
||||
|
||||
# 生成随机目标点击次数
|
||||
target_clicks = random.randint(Config.MIN_CLICK_COUNT, Config.MAX_CLICK_COUNT)
|
||||
|
||||
self.data['urls'][url] = {
|
||||
'url': url,
|
||||
'status': 'active', # active, completed, failed
|
||||
'target_clicks': target_clicks,
|
||||
'click_count': 0,
|
||||
'reply_count': 0,
|
||||
'created_time': datetime.now().isoformat(),
|
||||
'last_click_time': None,
|
||||
'click_history': []
|
||||
}
|
||||
|
||||
self._save_data()
|
||||
logger.info(f"成功添加URL,目标点击次数: {target_clicks}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"添加URL失败: {str(e)}")
|
||||
return False
|
||||
"""添加新URL到MySQL数据库"""
|
||||
site_id = self.site_mgr.add_site(
|
||||
site_url=url,
|
||||
site_name=url,
|
||||
site_dimension='MIP广告'
|
||||
)
|
||||
return site_id is not None
|
||||
|
||||
def record_click(self, url: str, has_reply: bool = False):
|
||||
"""
|
||||
记录一次点击
|
||||
|
||||
Args:
|
||||
url: URL
|
||||
has_reply: 是否获得回复
|
||||
"""
|
||||
"""记录一次点击到MySQL数据库"""
|
||||
try:
|
||||
if url not in self.data['urls']:
|
||||
site = self.site_mgr.get_site_by_url(url)
|
||||
if not site:
|
||||
logger.error(f"URL不存在: {url}")
|
||||
return
|
||||
|
||||
url_data = self.data['urls'][url]
|
||||
url_data['click_count'] += 1
|
||||
url_data['last_click_time'] = datetime.now().isoformat()
|
||||
click_id = self.click_mgr.record_click(
|
||||
site_id=site['id'],
|
||||
site_url=url
|
||||
)
|
||||
|
||||
if has_reply:
|
||||
url_data['reply_count'] += 1
|
||||
if click_id and has_reply:
|
||||
self.interaction_mgr.record_interaction(
|
||||
site_id=site['id'],
|
||||
click_id=click_id,
|
||||
interaction_type='reply',
|
||||
is_successful=True,
|
||||
response_received=True
|
||||
)
|
||||
|
||||
# 记录点击历史
|
||||
url_data['click_history'].append({
|
||||
'time': datetime.now().isoformat(),
|
||||
'has_reply': has_reply
|
||||
})
|
||||
|
||||
self._save_data()
|
||||
logger.info(f"记录点击成功,总点击次数: {url_data['click_count']}/{url_data['target_clicks']}")
|
||||
logger.info(f"记录点击成功: {url}, has_reply={has_reply}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"记录点击失败: {str(e)}")
|
||||
|
||||
def mark_url_completed(self, url: str):
|
||||
"""
|
||||
标记URL为已完成
|
||||
|
||||
Args:
|
||||
url: URL
|
||||
"""
|
||||
try:
|
||||
if url not in self.data['urls']:
|
||||
logger.error(f"URL不存在: {url}")
|
||||
return
|
||||
|
||||
self.data['urls'][url]['status'] = 'completed'
|
||||
self.data['urls'][url]['completed_time'] = datetime.now().isoformat()
|
||||
self._save_data()
|
||||
|
||||
logger.info(f"URL已完成: {url}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"标记URL完成失败: {str(e)}")
|
||||
|
||||
def mark_url_failed(self, url: str, reason: str = ""):
|
||||
"""
|
||||
标记URL为失败
|
||||
|
||||
Args:
|
||||
url: URL
|
||||
reason: 失败原因
|
||||
"""
|
||||
try:
|
||||
if url not in self.data['urls']:
|
||||
logger.error(f"URL不存在: {url}")
|
||||
return
|
||||
|
||||
self.data['urls'][url]['status'] = 'failed'
|
||||
self.data['urls'][url]['failed_reason'] = reason
|
||||
self.data['urls'][url]['failed_time'] = datetime.now().isoformat()
|
||||
self._save_data()
|
||||
|
||||
logger.warning(f"URL标记为失败: {url}, 原因: {reason}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"标记URL失败状态失败: {str(e)}")
|
||||
|
||||
|
||||
def get_active_urls(self) -> List[Dict]:
|
||||
"""
|
||||
获取所有活跃的URL
|
||||
|
||||
Returns:
|
||||
活跃URL列表
|
||||
"""
|
||||
try:
|
||||
active_urls = [
|
||||
data for data in self.data['urls'].values()
|
||||
if data['status'] == 'active'
|
||||
]
|
||||
return active_urls
|
||||
except Exception as e:
|
||||
logger.error(f"获取活跃URL失败: {str(e)}")
|
||||
return []
|
||||
"""获取所有活跃的URL"""
|
||||
return self.site_mgr.get_active_sites()
|
||||
|
||||
def get_url_info(self, url: str) -> Optional[Dict]:
|
||||
"""
|
||||
获取URL详细信息
|
||||
|
||||
Args:
|
||||
url: URL
|
||||
|
||||
Returns:
|
||||
URL信息
|
||||
"""
|
||||
return self.data['urls'].get(url)
|
||||
"""获取URL详细信息"""
|
||||
return self.site_mgr.get_site_by_url(url)
|
||||
|
||||
def get_all_urls(self) -> List[Dict]:
|
||||
"""
|
||||
获取所有URL
|
||||
|
||||
Returns:
|
||||
所有URL列表
|
||||
"""
|
||||
return list(self.data['urls'].values())
|
||||
"""获取所有URL"""
|
||||
return self.site_mgr.get_all_sites()
|
||||
|
||||
def get_statistics(self) -> Dict:
|
||||
"""
|
||||
获取统计数据
|
||||
|
||||
Returns:
|
||||
统计数据
|
||||
"""
|
||||
try:
|
||||
total_urls = len(self.data['urls'])
|
||||
active_urls = sum(1 for data in self.data['urls'].values() if data['status'] == 'active')
|
||||
completed_urls = sum(1 for data in self.data['urls'].values() if data['status'] == 'completed')
|
||||
failed_urls = sum(1 for data in self.data['urls'].values() if data['status'] == 'failed')
|
||||
|
||||
total_clicks = sum(data['click_count'] for data in self.data['urls'].values())
|
||||
total_replies = sum(data['reply_count'] for data in self.data['urls'].values())
|
||||
|
||||
stats = {
|
||||
'total_urls': total_urls,
|
||||
'active_urls': active_urls,
|
||||
'completed_urls': completed_urls,
|
||||
'failed_urls': failed_urls,
|
||||
'total_clicks': total_clicks,
|
||||
'total_replies': total_replies,
|
||||
'reply_rate': f"{(total_replies / total_clicks * 100) if total_clicks > 0 else 0:.2f}%"
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取统计数据失败: {str(e)}")
|
||||
return {}
|
||||
|
||||
def delete_url(self, url: str) -> bool:
|
||||
"""
|
||||
删除URL
|
||||
|
||||
Args:
|
||||
url: URL
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
if url in self.data['urls']:
|
||||
del self.data['urls'][url]
|
||||
self._save_data()
|
||||
logger.info(f"已删除URL: {url}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"URL不存在: {url}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"删除URL失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def reset_url(self, url: str) -> bool:
|
||||
"""
|
||||
重置URL状态(重新开始点击)
|
||||
|
||||
Args:
|
||||
url: URL
|
||||
|
||||
Returns:
|
||||
是否重置成功
|
||||
"""
|
||||
try:
|
||||
if url not in self.data['urls']:
|
||||
logger.warning(f"URL不存在: {url}")
|
||||
return False
|
||||
|
||||
# 生成新的随机目标点击次数
|
||||
target_clicks = random.randint(Config.MIN_CLICK_COUNT, Config.MAX_CLICK_COUNT)
|
||||
|
||||
self.data['urls'][url]['status'] = 'active'
|
||||
self.data['urls'][url]['target_clicks'] = target_clicks
|
||||
self.data['urls'][url]['click_count'] = 0
|
||||
self.data['urls'][url]['reply_count'] = 0
|
||||
self.data['urls'][url]['last_click_time'] = None
|
||||
self.data['urls'][url]['click_history'] = []
|
||||
self.data['urls'][url]['reset_time'] = datetime.now().isoformat()
|
||||
|
||||
self._save_data()
|
||||
logger.info(f"已重置URL: {url}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"重置URL失败: {str(e)}")
|
||||
return False
|
||||
"""获取统计数据"""
|
||||
return self.stats_mgr.get_statistics()
|
||||
|
||||
|
||||
51
db/ai_mip_click.sql
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : mixue
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 90001 (9.0.1)
|
||||
Source Host : localhost:3306
|
||||
Source Schema : ai_article
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 90001 (9.0.1)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 12/01/2026 20:31:43
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_click
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_mip_click`;
|
||||
CREATE TABLE `ai_mip_click` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`site_id` bigint NOT NULL COMMENT '关联站点ID(外键指向 ai_mip_site.id)',
|
||||
`site_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '网站URL(冗余字段,便于查询优化)',
|
||||
`click_time` datetime NOT NULL COMMENT '点击发生时间',
|
||||
`user_ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户IP地址',
|
||||
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '浏览器/设备信息',
|
||||
`referer_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '来源页面URL',
|
||||
`device_type` enum('mobile','pc','tablet') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备类型',
|
||||
`click_count` int NULL DEFAULT 1 COMMENT '本次点击事件的计数(一般为1,可用于批量插入)',
|
||||
`is_valid` tinyint(1) NULL DEFAULT 1 COMMENT '是否有效点击(防刷)',
|
||||
`task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'RPA任务ID(可选)',
|
||||
`operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作者(如自动系统)',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_site_id`(`site_id` ASC) USING BTREE,
|
||||
INDEX `idx_click_time`(`click_time` ASC) USING BTREE,
|
||||
INDEX `idx_site_url`(`site_url` ASC) USING BTREE,
|
||||
INDEX `idx_click_time_site`(`click_time` ASC, `site_id` ASC) USING BTREE,
|
||||
INDEX `idx_task_id`(`task_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'MIP页广告点击日志表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of ai_mip_click
|
||||
-- ----------------------------
|
||||
INSERT INTO `ai_mip_click` VALUES (1, 1, 'https://example.com', '2026-01-12 20:25:09', NULL, NULL, NULL, NULL, 1, 1, 'TASK20260112001', 'RPA_SYSTEM', '2026-01-12 20:25:09');
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
75
db/ai_mip_interaction.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : mixue
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 90001 (9.0.1)
|
||||
Source Host : localhost:3306
|
||||
Source Schema : ai_article
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 90001 (9.0.1)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 12/01/2026 20:31:30
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_interaction
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_mip_interaction`;
|
||||
CREATE TABLE `ai_mip_interaction` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`site_id` bigint NOT NULL COMMENT '关联站点ID',
|
||||
`click_id` bigint NULL DEFAULT NULL COMMENT '关联点击记录ID',
|
||||
`task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'RPA任务ID',
|
||||
`interaction_type` enum('reply','comment','message','form_submit','follow','like','share') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '互动类型',
|
||||
`interaction_time` datetime NOT NULL COMMENT '互动发生时间',
|
||||
`interaction_status` enum('pending','success','failed','skipped') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending' COMMENT '互动状态',
|
||||
`reply_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '回复/评论的内容',
|
||||
`reply_template_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '使用的回复模板ID',
|
||||
`ad_element_xpath` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '广告元素的XPath定位',
|
||||
`ad_element_selector` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '广告元素的CSS选择器',
|
||||
`ad_text_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '广告的文本内容',
|
||||
`execution_mode` enum('auto','manual','semi_auto') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'auto' COMMENT '执行方式',
|
||||
`rpa_script` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '使用的RPA脚本名称',
|
||||
`browser_type` enum('headless','headed','playwright','selenium') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '浏览器类型',
|
||||
`anti_detection_method` json NULL COMMENT '万金油技术方案',
|
||||
`proxy_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '使用的代理IP',
|
||||
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '使用的User-Agent',
|
||||
`custom_headers` json NULL COMMENT '自定义HTTP头',
|
||||
`fingerprint_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '浏览器指纹ID',
|
||||
`response_received` tinyint(1) NULL DEFAULT 0 COMMENT '是否收到回复',
|
||||
`response_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '对方回复的内容',
|
||||
`response_time` datetime NULL DEFAULT NULL COMMENT '收到回复的时间',
|
||||
`response_delay_seconds` int NULL DEFAULT NULL COMMENT '回复延迟(秒)',
|
||||
`is_successful` tinyint(1) NULL DEFAULT 0 COMMENT '是否成功互动',
|
||||
`error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '失败原因/错误信息',
|
||||
`retry_count` int NULL DEFAULT 0 COMMENT '重试次数',
|
||||
`conversion_flag` tinyint(1) NULL DEFAULT 0 COMMENT '是否产生转化',
|
||||
`site_dimension` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '网址维度标签',
|
||||
`campaign_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '广告活动ID',
|
||||
`operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作者',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
|
||||
`remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注信息',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_site_id`(`site_id` ASC) USING BTREE,
|
||||
INDEX `idx_click_id`(`click_id` ASC) USING BTREE,
|
||||
INDEX `idx_task_id`(`task_id` ASC) USING BTREE,
|
||||
INDEX `idx_interaction_time`(`interaction_time` ASC) USING BTREE,
|
||||
INDEX `idx_interaction_status`(`interaction_status` ASC) USING BTREE,
|
||||
INDEX `idx_composite`(`site_id` ASC, `interaction_time` ASC, `interaction_status` ASC) USING BTREE,
|
||||
INDEX `idx_response_received`(`response_received` ASC) USING BTREE,
|
||||
INDEX `idx_conversion`(`conversion_flag` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'MIP页广告互动回复日志表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of ai_mip_interaction
|
||||
-- ----------------------------
|
||||
INSERT INTO `ai_mip_interaction` VALUES (1, 1, 1, 'TASK20260112001', 'reply', '2026-01-12 20:25:09', 'success', '您好,请问有什么可以帮助您的吗?', NULL, NULL, NULL, NULL, 'auto', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, NULL, 0, 0, NULL, NULL, NULL, '2026-01-12 20:25:09', '2026-01-12 20:25:09', NULL);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
55
db/ai_mip_site.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : mixue
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 90001 (9.0.1)
|
||||
Source Host : localhost:3306
|
||||
Source Schema : ai_article
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 90001 (9.0.1)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 12/01/2026 20:31:23
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_site
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_mip_site`;
|
||||
CREATE TABLE `ai_mip_site` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`site_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '网站URL,唯一',
|
||||
`site_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '网站名称(可选)',
|
||||
`status` enum('active','inactive','pending') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'active' COMMENT '状态:激活/停用/待审核',
|
||||
`frequency` int NULL DEFAULT 1 COMMENT '频次(如每小时发几次)',
|
||||
`time_start` time NULL DEFAULT '00:00:00' COMMENT '开始时间(HH:MM:SS)',
|
||||
`time_end` time NULL DEFAULT '23:59:59' COMMENT '结束时间(HH:MM:SS)',
|
||||
`interval_minutes` int NULL DEFAULT 60 COMMENT '执行间隔(分钟)',
|
||||
`ad_feature` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '广告特征描述(JSON格式,如:{\"color\":\"red\", \"position\":\"top\"})',
|
||||
`click_count` bigint NULL DEFAULT 0 COMMENT '累计点击次数',
|
||||
`reply_count` bigint NULL DEFAULT 0 COMMENT '累计回复次数',
|
||||
`site_dimension` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '网址维度标签(如:教育、医疗等)',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`created_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '创建人',
|
||||
`updated_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新人',
|
||||
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注信息',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `site_url`(`site_url` ASC) USING BTREE,
|
||||
UNIQUE INDEX `idx_site_url`(`site_url`(191) ASC) USING BTREE,
|
||||
INDEX `idx_status`(`status` ASC) USING BTREE,
|
||||
INDEX `idx_created_at`(`created_at` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'MIP页广告网址管理表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of ai_mip_site
|
||||
-- ----------------------------
|
||||
INSERT INTO `ai_mip_site` VALUES (1, 'https://example.com', '示例网站1', 'active', 1, '00:00:00', '23:59:59', 60, NULL, 0, 0, '教育', '2026-01-12 20:24:18', '2026-01-12 20:24:18', 'admin', NULL, NULL);
|
||||
INSERT INTO `ai_mip_site` VALUES (2, 'https://test.com', '测试网站2', 'active', 1, '00:00:00', '23:59:59', 60, NULL, 0, 0, '医疗', '2026-01-12 20:24:18', '2026-01-12 20:24:18', 'admin', NULL, NULL);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
100
db/init_databases.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SQLite数据库初始化脚本
|
||||
自动创建开发环境(ai_mip_dev.db)和生产环境(ai_mip_prod.db)数据库
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 数据库文件路径
|
||||
DB_DIR = Path(__file__).parent
|
||||
DEV_DB = DB_DIR / "ai_mip_dev.db"
|
||||
PROD_DB = DB_DIR / "ai_mip_prod.db"
|
||||
|
||||
# SQL脚本路径
|
||||
INIT_SQL = DB_DIR / "init_sqlite.sql"
|
||||
SEED_DEV_SQL = DB_DIR / "seed_dev.sql"
|
||||
|
||||
|
||||
def execute_sql_file(conn, sql_file):
|
||||
"""执行SQL文件"""
|
||||
with open(sql_file, 'r', encoding='utf-8') as f:
|
||||
sql_script = f.read()
|
||||
|
||||
# SQLite需要逐条执行语句
|
||||
conn.executescript(sql_script)
|
||||
conn.commit()
|
||||
print(f"✓ 已执行: {sql_file.name}")
|
||||
|
||||
|
||||
def init_database(db_path, with_seed=False):
|
||||
"""初始化数据库"""
|
||||
# 如果数据库已存在,询问是否覆盖
|
||||
if db_path.exists():
|
||||
response = input(f"\n数据库 {db_path.name} 已存在,是否覆盖? (y/n): ").strip().lower()
|
||||
if response != 'y':
|
||||
print(f"跳过 {db_path.name}")
|
||||
return
|
||||
os.remove(db_path)
|
||||
print(f"已删除旧数据库: {db_path.name}")
|
||||
|
||||
print(f"\n创建数据库: {db_path.name}")
|
||||
|
||||
# 连接数据库(自动创建)
|
||||
conn = sqlite3.connect(db_path)
|
||||
|
||||
try:
|
||||
# 执行初始化SQL
|
||||
execute_sql_file(conn, INIT_SQL)
|
||||
|
||||
# 如果需要,执行种子数据
|
||||
if with_seed:
|
||||
execute_sql_file(conn, SEED_DEV_SQL)
|
||||
|
||||
print(f"✓ 数据库 {db_path.name} 创建成功")
|
||||
|
||||
# 验证表是否创建成功
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = cursor.fetchall()
|
||||
print(f" 创建的表: {', '.join([t[0] for t in tables])}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 创建数据库失败: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("SQLite数据库初始化工具")
|
||||
print("=" * 60)
|
||||
|
||||
# 检查SQL文件是否存在
|
||||
if not INIT_SQL.exists():
|
||||
print(f"错误: 找不到初始化脚本 {INIT_SQL}")
|
||||
return
|
||||
|
||||
# 初始化开发数据库(带测试数据)
|
||||
print("\n[1] 初始化开发环境数据库")
|
||||
init_database(DEV_DB, with_seed=True)
|
||||
|
||||
# 初始化生产数据库(不带测试数据)
|
||||
print("\n[2] 初始化生产环境数据库")
|
||||
init_database(PROD_DB, with_seed=False)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("数据库初始化完成")
|
||||
print("=" * 60)
|
||||
print(f"开发数据库: {DEV_DB}")
|
||||
print(f"生产数据库: {PROD_DB}")
|
||||
print("\n使用方法:")
|
||||
print(" 开发环境: 在 .env.development 中设置 DATABASE_PATH=db/ai_mip_dev.db")
|
||||
print(" 生产环境: 在 .env.production 中设置 DATABASE_PATH=db/ai_mip_prod.db")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
125
db/init_sqlite.sql
Normal file
@@ -0,0 +1,125 @@
|
||||
-- SQLite数据库初始化脚本
|
||||
-- 适用于开发环境(ai_mip_dev.db)和生产环境(ai_mip_prod.db)
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_site
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS ai_mip_site;
|
||||
CREATE TABLE ai_mip_site (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_url TEXT NOT NULL UNIQUE,
|
||||
site_name TEXT,
|
||||
status TEXT CHECK(status IN ('active', 'inactive', 'pending')) DEFAULT 'active',
|
||||
frequency INTEGER DEFAULT 1,
|
||||
time_start TEXT DEFAULT '00:00:00',
|
||||
time_end TEXT DEFAULT '23:59:59',
|
||||
interval_minutes INTEGER DEFAULT 60,
|
||||
ad_feature TEXT,
|
||||
click_count INTEGER DEFAULT 0,
|
||||
reply_count INTEGER DEFAULT 0,
|
||||
site_dimension TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by TEXT,
|
||||
updated_by TEXT,
|
||||
remark TEXT
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE UNIQUE INDEX idx_site_url ON ai_mip_site(site_url);
|
||||
CREATE INDEX idx_status ON ai_mip_site(status);
|
||||
CREATE INDEX idx_created_at ON ai_mip_site(created_at);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_click
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS ai_mip_click;
|
||||
CREATE TABLE ai_mip_click (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id INTEGER NOT NULL,
|
||||
site_url TEXT NOT NULL,
|
||||
click_time DATETIME NOT NULL,
|
||||
user_ip TEXT,
|
||||
user_agent TEXT,
|
||||
referer_url TEXT,
|
||||
device_type TEXT CHECK(device_type IN ('mobile', 'pc', 'tablet')),
|
||||
click_count INTEGER DEFAULT 1,
|
||||
is_valid INTEGER DEFAULT 1,
|
||||
task_id TEXT,
|
||||
operator TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (site_id) REFERENCES ai_mip_site(id)
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_site_id ON ai_mip_click(site_id);
|
||||
CREATE INDEX idx_click_time ON ai_mip_click(click_time);
|
||||
CREATE INDEX idx_site_url_click ON ai_mip_click(site_url);
|
||||
CREATE INDEX idx_click_time_site ON ai_mip_click(click_time, site_id);
|
||||
CREATE INDEX idx_task_id ON ai_mip_click(task_id);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_interaction
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS ai_mip_interaction;
|
||||
CREATE TABLE ai_mip_interaction (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id INTEGER NOT NULL,
|
||||
click_id INTEGER,
|
||||
task_id TEXT,
|
||||
interaction_type TEXT CHECK(interaction_type IN ('reply', 'comment', 'message', 'form_submit', 'follow', 'like', 'share')) NOT NULL,
|
||||
interaction_time DATETIME NOT NULL,
|
||||
interaction_status TEXT CHECK(interaction_status IN ('pending', 'success', 'failed', 'skipped')) DEFAULT 'pending',
|
||||
reply_content TEXT,
|
||||
reply_template_id TEXT,
|
||||
ad_element_xpath TEXT,
|
||||
ad_element_selector TEXT,
|
||||
ad_text_content TEXT,
|
||||
execution_mode TEXT CHECK(execution_mode IN ('auto', 'manual', 'semi_auto')) DEFAULT 'auto',
|
||||
rpa_script TEXT,
|
||||
browser_type TEXT CHECK(browser_type IN ('headless', 'headed', 'playwright', 'selenium')),
|
||||
anti_detection_method TEXT,
|
||||
proxy_ip TEXT,
|
||||
user_agent TEXT,
|
||||
custom_headers TEXT,
|
||||
fingerprint_id TEXT,
|
||||
response_received INTEGER DEFAULT 0,
|
||||
response_content TEXT,
|
||||
response_time DATETIME,
|
||||
response_delay_seconds INTEGER,
|
||||
is_successful INTEGER DEFAULT 0,
|
||||
error_message TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
conversion_flag INTEGER DEFAULT 0,
|
||||
site_dimension TEXT,
|
||||
campaign_id TEXT,
|
||||
operator TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
remark TEXT,
|
||||
FOREIGN KEY (site_id) REFERENCES ai_mip_site(id),
|
||||
FOREIGN KEY (click_id) REFERENCES ai_mip_click(id)
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_site_id_interaction ON ai_mip_interaction(site_id);
|
||||
CREATE INDEX idx_click_id_interaction ON ai_mip_interaction(click_id);
|
||||
CREATE INDEX idx_task_id_interaction ON ai_mip_interaction(task_id);
|
||||
CREATE INDEX idx_interaction_time ON ai_mip_interaction(interaction_time);
|
||||
CREATE INDEX idx_interaction_status ON ai_mip_interaction(interaction_status);
|
||||
CREATE INDEX idx_composite ON ai_mip_interaction(site_id, interaction_time, interaction_status);
|
||||
CREATE INDEX idx_response_received ON ai_mip_interaction(response_received);
|
||||
CREATE INDEX idx_conversion ON ai_mip_interaction(conversion_flag);
|
||||
|
||||
-- 创建触发器:自动更新 updated_at
|
||||
CREATE TRIGGER update_ai_mip_site_timestamp
|
||||
AFTER UPDATE ON ai_mip_site
|
||||
BEGIN
|
||||
UPDATE ai_mip_site SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER update_ai_mip_interaction_timestamp
|
||||
AFTER UPDATE ON ai_mip_interaction
|
||||
BEGIN
|
||||
UPDATE ai_mip_interaction SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
145
db/mip_table.txt
Normal file
@@ -0,0 +1,145 @@
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_click
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_mip_click`;
|
||||
CREATE TABLE `ai_mip_click` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`site_id` bigint NOT NULL COMMENT '关联站点ID(外键指向 ai_mip_site.id)',
|
||||
`site_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '网站URL(冗余字段,便于查询优化)',
|
||||
`click_time` datetime NOT NULL COMMENT '点击发生时间',
|
||||
`user_ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户IP地址',
|
||||
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '浏览器/设备信息',
|
||||
`referer_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '来源页面URL',
|
||||
`device_type` enum('mobile','pc','tablet') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备类型',
|
||||
`click_count` int NULL DEFAULT 1 COMMENT '本次点击事件的计数(一般为1,可用于批量插入)',
|
||||
`is_valid` tinyint(1) NULL DEFAULT 1 COMMENT '是否有效点击(防刷)',
|
||||
`task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'RPA任务ID(可选)',
|
||||
`operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作者(如自动系统)',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_site_id`(`site_id` ASC) USING BTREE,
|
||||
INDEX `idx_click_time`(`click_time` ASC) USING BTREE,
|
||||
INDEX `idx_site_url`(`site_url` ASC) USING BTREE,
|
||||
INDEX `idx_click_time_site`(`click_time` ASC, `site_id` ASC) USING BTREE,
|
||||
INDEX `idx_task_id`(`task_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'MIP页广告点击日志表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_interaction
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_mip_interaction`;
|
||||
CREATE TABLE `ai_mip_interaction` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`site_id` bigint NOT NULL COMMENT '关联站点ID',
|
||||
`click_id` bigint NULL DEFAULT NULL COMMENT '关联点击记录ID',
|
||||
`task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'RPA任务ID',
|
||||
`interaction_type` enum('reply','comment','message','form_submit','follow','like','share') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '互动类型',
|
||||
`interaction_time` datetime NOT NULL COMMENT '互动发生时间',
|
||||
`interaction_status` enum('pending','success','failed','skipped') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending' COMMENT '互动状态',
|
||||
`reply_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '回复/评论的内容',
|
||||
`reply_template_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '使用的回复模板ID',
|
||||
`ad_element_xpath` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '广告元素的XPath定位',
|
||||
`ad_element_selector` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '广告元素的CSS选择器',
|
||||
`ad_text_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '广告的文本内容',
|
||||
`execution_mode` enum('auto','manual','semi_auto') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'auto' COMMENT '执行方式',
|
||||
`rpa_script` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '使用的RPA脚本名称',
|
||||
`browser_type` enum('headless','headed','playwright','selenium') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '浏览器类型',
|
||||
`anti_detection_method` json NULL COMMENT '万金油技术方案',
|
||||
`proxy_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '使用的代理IP',
|
||||
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '使用的User-Agent',
|
||||
`custom_headers` json NULL COMMENT '自定义HTTP头',
|
||||
`fingerprint_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '浏览器指纹ID',
|
||||
`response_received` tinyint(1) NULL DEFAULT 0 COMMENT '是否收到回复',
|
||||
`response_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '对方回复的内容',
|
||||
`response_time` datetime NULL DEFAULT NULL COMMENT '收到回复的时间',
|
||||
`response_delay_seconds` int NULL DEFAULT NULL COMMENT '回复延迟(秒)',
|
||||
`is_successful` tinyint(1) NULL DEFAULT 0 COMMENT '是否成功互动',
|
||||
`error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '失败原因/错误信息',
|
||||
`retry_count` int NULL DEFAULT 0 COMMENT '重试次数',
|
||||
`conversion_flag` tinyint(1) NULL DEFAULT 0 COMMENT '是否产生转化',
|
||||
`site_dimension` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '网址维度标签',
|
||||
`campaign_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '广告活动ID',
|
||||
`operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作者',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
|
||||
`remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注信息',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_site_id`(`site_id` ASC) USING BTREE,
|
||||
INDEX `idx_click_id`(`click_id` ASC) USING BTREE,
|
||||
INDEX `idx_task_id`(`task_id` ASC) USING BTREE,
|
||||
INDEX `idx_interaction_time`(`interaction_time` ASC) USING BTREE,
|
||||
INDEX `idx_interaction_status`(`interaction_status` ASC) USING BTREE,
|
||||
INDEX `idx_composite`(`site_id` ASC, `interaction_time` ASC, `interaction_status` ASC) USING BTREE,
|
||||
INDEX `idx_response_received`(`response_received` ASC) USING BTREE,
|
||||
INDEX `idx_conversion`(`conversion_flag` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'MIP页广告互动回复日志表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_site
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_mip_site`;
|
||||
CREATE TABLE `ai_mip_site` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`site_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '网站URL,唯一',
|
||||
`site_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '网站名称(可选)',
|
||||
`status` enum('active','inactive','pending') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'active' COMMENT '状态:激活/停用/待审核',
|
||||
`frequency` int NULL DEFAULT 1 COMMENT '频次(如每小时发几次)',
|
||||
`time_start` time NULL DEFAULT '00:00:00' COMMENT '开始时间(HH:MM:SS)',
|
||||
`time_end` time NULL DEFAULT '23:59:59' COMMENT '结束时间(HH:MM:SS)',
|
||||
`interval_minutes` int NULL DEFAULT 60 COMMENT '执行间隔(分钟)',
|
||||
`ad_feature` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '广告特征描述(JSON格式,如:{\"color\":\"red\", \"position\":\"top\"})',
|
||||
`click_count` bigint NULL DEFAULT 0 COMMENT '累计点击次数',
|
||||
`reply_count` bigint NULL DEFAULT 0 COMMENT '累计回复次数',
|
||||
`site_dimension` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '网址维度标签(如:教育、医疗等)',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`created_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '创建人',
|
||||
`updated_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新人',
|
||||
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注信息',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `site_url`(`site_url` ASC) USING BTREE,
|
||||
UNIQUE INDEX `idx_site_url`(`site_url`(191) ASC) USING BTREE,
|
||||
INDEX `idx_status`(`status` ASC) USING BTREE,
|
||||
INDEX `idx_created_at`(`created_at` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'MIP页广告网址管理表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ai_mip_task_log
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_mip_task_log`;
|
||||
CREATE TABLE `ai_mip_task_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'RPA任务唯一ID',
|
||||
`site_id` bigint NOT NULL COMMENT '关联站点ID',
|
||||
`step_1_visit_time` datetime NULL DEFAULT NULL COMMENT '步骤1:访问网址时间',
|
||||
`step_1_status` enum('success','failed','skipped') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '步骤1状态',
|
||||
`step_2_antibot_time` datetime NULL DEFAULT NULL COMMENT '步骤2:万金油技术方案执行时间',
|
||||
`step_2_status` enum('success','failed','skipped') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '步骤2状态',
|
||||
`step_3_ad_detection_time` datetime NULL DEFAULT NULL COMMENT '步骤3:广告检测时间',
|
||||
`step_3_has_ad` tinyint(1) NULL DEFAULT NULL COMMENT '是否检测到广告',
|
||||
`step_3_ad_count` int NULL DEFAULT 0 COMMENT '检测到的广告数量',
|
||||
`step_4_click_time` datetime NULL DEFAULT NULL COMMENT '步骤4:点击广告时间',
|
||||
`step_4_status` enum('success','failed','skipped') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '步骤4状态',
|
||||
`step_5_reply_time` datetime NULL DEFAULT NULL COMMENT '步骤5:获取回复时间',
|
||||
`step_5_status` enum('success','failed','skipped') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '步骤5状态',
|
||||
`task_start_time` datetime NOT NULL COMMENT '任务开始时间',
|
||||
`task_end_time` datetime NULL DEFAULT NULL COMMENT '任务结束时间',
|
||||
`task_duration_seconds` int NULL DEFAULT NULL COMMENT '任务执行时长(秒)',
|
||||
`task_status` enum('running','completed','failed','timeout') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'running' COMMENT '任务整体状态',
|
||||
`total_clicks` int NULL DEFAULT 0 COMMENT '本次任务总点击次数',
|
||||
`total_interactions` int NULL DEFAULT 0 COMMENT '本次任务总互动次数',
|
||||
`successful_interactions` int NULL DEFAULT 0 COMMENT '成功互动次数',
|
||||
`failed_interactions` int NULL DEFAULT 0 COMMENT '失败互动次数',
|
||||
`execution_mode` enum('auto','manual','scheduled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'auto' COMMENT '执行模式',
|
||||
`triggered_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '触发者(定时任务/手动触发/队列)',
|
||||
`error_log` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '错误日志',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `task_id`(`task_id` ASC) USING BTREE,
|
||||
UNIQUE INDEX `uk_task_id`(`task_id` ASC) USING BTREE,
|
||||
INDEX `idx_site_id`(`site_id` ASC) USING BTREE,
|
||||
INDEX `idx_task_status`(`task_status` ASC) USING BTREE,
|
||||
INDEX `idx_start_time`(`task_start_time` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'RPA任务执行日志表' ROW_FORMAT = DYNAMIC;
|
||||
20
db/seed_dev.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- 开发环境测试数据
|
||||
-- 用于 ai_mip_dev.db
|
||||
|
||||
-- 插入测试站点
|
||||
INSERT INTO ai_mip_site (site_url, site_name, status, frequency, time_start, time_end, interval_minutes, click_count, reply_count, site_dimension, created_by) VALUES
|
||||
('https://health.baidu.com/m/detail/ar_2366617956693492811', '百度健康测试页面', 'active', 3, '09:00:00', '21:00:00', 45, 0, 0, '医疗健康', 'admin'),
|
||||
('https://example.com/test', '测试网站1', 'active', 2, '10:00:00', '20:00:00', 60, 0, 0, '教育', 'admin'),
|
||||
('https://demo.com/page', '演示网站', 'inactive', 1, '00:00:00', '23:59:59', 120, 0, 0, '商业', 'admin');
|
||||
|
||||
-- 插入测试点击记录
|
||||
INSERT INTO ai_mip_click (site_id, site_url, click_time, user_ip, device_type, task_id, operator) VALUES
|
||||
(1, 'https://health.baidu.com/m/detail/ar_2366617956693492811', datetime('now'), '192.168.1.100', 'pc', 'TASK_DEV_001', 'RPA_SYSTEM'),
|
||||
(1, 'https://health.baidu.com/m/detail/ar_2366617956693492811', datetime('now', '-1 hour'), '192.168.1.101', 'mobile', 'TASK_DEV_002', 'RPA_SYSTEM'),
|
||||
(2, 'https://example.com/test', datetime('now', '-2 hours'), '192.168.1.102', 'pc', 'TASK_DEV_003', 'RPA_SYSTEM');
|
||||
|
||||
-- 插入测试互动记录
|
||||
INSERT INTO ai_mip_interaction (site_id, click_id, task_id, interaction_type, interaction_time, interaction_status, reply_content, execution_mode, browser_type, is_successful, operator) VALUES
|
||||
(1, 1, 'TASK_DEV_001', 'reply', datetime('now'), 'success', '测试回复内容', 'auto', 'playwright', 1, 'RPA_SYSTEM'),
|
||||
(1, 2, 'TASK_DEV_002', 'comment', datetime('now', '-1 hour'), 'success', '测试评论内容', 'auto', 'playwright', 1, 'RPA_SYSTEM'),
|
||||
(2, 3, 'TASK_DEV_003', 'reply', datetime('now', '-2 hours'), 'pending', NULL, 'auto', 'playwright', 0, 'RPA_SYSTEM');
|
||||
547
db_manager.py
Normal file
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
数据库管理器
|
||||
使用 MySQL 数据库
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional, Union
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
from config import Config
|
||||
|
||||
try:
|
||||
import pymysql
|
||||
MYSQL_AVAILABLE = True
|
||||
except ImportError:
|
||||
MYSQL_AVAILABLE = False
|
||||
logger.error("pymysql 未安装,请安装: pip install pymysql")
|
||||
raise ImportError("使用 MySQL 需要安装 pymysql: pip install pymysql")
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
"""数据库管理器,使用 MySQL"""
|
||||
|
||||
def __init__(self, db_path: str = None):
|
||||
"""
|
||||
初始化数据库连接
|
||||
|
||||
Args:
|
||||
db_path: 忽略,仅为了兼容性
|
||||
"""
|
||||
if not MYSQL_AVAILABLE:
|
||||
raise ImportError("使用 MySQL 需要安装 pymysql: pip install pymysql")
|
||||
|
||||
self.db_config = {
|
||||
'host': Config.MYSQL_HOST,
|
||||
'port': Config.MYSQL_PORT,
|
||||
'user': Config.MYSQL_USER,
|
||||
'password': Config.MYSQL_PASSWORD,
|
||||
'database': Config.MYSQL_DATABASE,
|
||||
'charset': 'utf8mb4'
|
||||
}
|
||||
logger.info(f"MySQL数据库初始化: {Config.MYSQL_HOST}:{Config.MYSQL_PORT}/{Config.MYSQL_DATABASE}")
|
||||
|
||||
def get_connection(self) -> 'pymysql.Connection':
|
||||
"""获取MySQL数据库连接"""
|
||||
conn = pymysql.connect(**self.db_config)
|
||||
return conn
|
||||
|
||||
def _dict_from_row(self, row) -> Dict:
|
||||
"""将数据库行转换为字典"""
|
||||
if row is None:
|
||||
return None
|
||||
return dict(row) if isinstance(row, dict) else row
|
||||
|
||||
def _get_placeholder(self) -> str:
|
||||
"""获取SQL占位符,MySQL使用 %s"""
|
||||
return '%s'
|
||||
|
||||
def _execute_query(self, conn, sql: str, params: tuple = None):
|
||||
"""执行SQL查询,使用DictCursor"""
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
if params:
|
||||
cursor.execute(sql, params)
|
||||
else:
|
||||
cursor.execute(sql)
|
||||
|
||||
return cursor
|
||||
|
||||
|
||||
class SiteManager(DatabaseManager):
|
||||
"""站点管理"""
|
||||
|
||||
def add_site(self, site_url: str, site_name: str = None,
|
||||
site_dimension: str = None, frequency: int = None,
|
||||
time_start: str = None, time_end: str = None,
|
||||
interval_minutes: int = None) -> Optional[int]:
|
||||
"""
|
||||
添加新站点
|
||||
|
||||
Args:
|
||||
site_url: 网站URL
|
||||
site_name: 网站名称
|
||||
site_dimension: 网站维度标签
|
||||
frequency: 频次
|
||||
time_start: 开始时间
|
||||
time_end: 结束时间
|
||||
interval_minutes: 执行间隔(分钟)
|
||||
|
||||
Returns:
|
||||
站点ID,失败返回None
|
||||
"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
# 生成随机目标点击次数(兼容原有逻辑)
|
||||
target_clicks = random.randint(
|
||||
getattr(Config, 'MIN_CLICK_COUNT', 1),
|
||||
getattr(Config, 'MAX_CLICK_COUNT', 10)
|
||||
)
|
||||
|
||||
sql = f"""
|
||||
INSERT INTO ai_mip_site (
|
||||
site_url, site_name, status, frequency,
|
||||
time_start, time_end, interval_minutes,
|
||||
site_dimension, created_by
|
||||
) VALUES ({ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph})
|
||||
"""
|
||||
|
||||
cursor.execute(sql, (
|
||||
site_url,
|
||||
site_name or site_url,
|
||||
'active',
|
||||
frequency or 1,
|
||||
time_start or '09:00:00',
|
||||
time_end or '21:00:00',
|
||||
interval_minutes or 60,
|
||||
site_dimension,
|
||||
'system'
|
||||
))
|
||||
|
||||
site_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"成功添加站点: {site_url} (ID: {site_id})")
|
||||
return site_id
|
||||
|
||||
except pymysql.IntegrityError:
|
||||
logger.warning(f"站点URL已存在: {site_url}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"添加站点失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_site_by_url(self, site_url: str) -> Optional[Dict]:
|
||||
"""根据URL获取站点信息"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
cursor = self._execute_query(conn, f"SELECT * FROM ai_mip_site WHERE site_url = {ph}", (site_url,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
return self._dict_from_row(row) if row else None
|
||||
except Exception as e:
|
||||
logger.error(f"查询站点失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_site_by_id(self, site_id: int) -> Optional[Dict]:
|
||||
"""根据ID获取站点信息"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
cursor = self._execute_query(conn, f"SELECT * FROM ai_mip_site WHERE id = {ph}", (site_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
return self._dict_from_row(row) if row else None
|
||||
except Exception as e:
|
||||
logger.error(f"查询站点失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_active_sites(self) -> List[Dict]:
|
||||
"""获取所有活跃站点"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
cursor = self._execute_query(conn, "SELECT * FROM ai_mip_site WHERE status = 'active' ORDER BY created_at DESC")
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [self._dict_from_row(row) for row in rows]
|
||||
except Exception as e:
|
||||
logger.error(f"查询活跃站点失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_all_sites(self) -> List[Dict]:
|
||||
"""获取所有站点"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
cursor = self._execute_query(conn, "SELECT * FROM ai_mip_site ORDER BY created_at DESC")
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [self._dict_from_row(row) for row in rows]
|
||||
except Exception as e:
|
||||
logger.error(f"查询所有站点失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def update_site_status(self, site_id: int, status: str) -> bool:
|
||||
"""更新站点状态"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
cursor = self._execute_query(
|
||||
conn,
|
||||
f"UPDATE ai_mip_site SET status = {ph}, updated_by = {ph} WHERE id = {ph}",
|
||||
(status, 'system', site_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info(f"更新站点状态: ID={site_id}, status={status}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"更新站点状态失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def increment_click_count(self, site_id: int, count: int = 1) -> bool:
|
||||
"""增加点击次数"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
cursor = self._execute_query(
|
||||
conn,
|
||||
f"UPDATE ai_mip_site SET click_count = click_count + {ph} WHERE id = {ph}",
|
||||
(count, site_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"更新点击次数失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def increment_reply_count(self, site_id: int, count: int = 1) -> bool:
|
||||
"""增加回复次数"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
cursor = self._execute_query(
|
||||
conn,
|
||||
f"UPDATE ai_mip_site SET reply_count = reply_count + {ph} WHERE id = {ph}",
|
||||
(count, site_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"更新回复次数失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def delete_site(self, site_id: int) -> bool:
|
||||
"""删除站点"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
cursor = self._execute_query(conn, f"DELETE FROM ai_mip_site WHERE id = {ph}", (site_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info(f"已删除站点: ID={site_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"删除站点失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
class ClickManager(DatabaseManager):
|
||||
"""点击记录管理"""
|
||||
|
||||
def record_click(self, site_id: int, site_url: str,
|
||||
user_ip: str = None, device_type: str = 'pc',
|
||||
task_id: str = None) -> Optional[int]:
|
||||
"""
|
||||
记录一次点击
|
||||
|
||||
Args:
|
||||
site_id: 站点ID
|
||||
site_url: 站点URL
|
||||
user_ip: 用户IP(代理IP)
|
||||
device_type: 设备类型
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
点击记录ID
|
||||
"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
sql = f"""
|
||||
INSERT INTO ai_mip_click (
|
||||
site_id, site_url, click_time, user_ip,
|
||||
device_type, task_id, operator
|
||||
) VALUES ({ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph})
|
||||
"""
|
||||
|
||||
cursor.execute(sql, (
|
||||
site_id,
|
||||
site_url,
|
||||
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
user_ip,
|
||||
device_type,
|
||||
task_id or f'TASK_{datetime.now().strftime("%Y%m%d%H%M%S")}',
|
||||
'RPA_SYSTEM'
|
||||
))
|
||||
|
||||
click_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# 更新站点点击次数
|
||||
site_mgr = SiteManager()
|
||||
site_mgr.increment_click_count(site_id)
|
||||
|
||||
logger.info(f"记录点击: site_id={site_id}, click_id={click_id}")
|
||||
return click_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"记录点击失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_clicks_by_site(self, site_id: int, limit: int = 100) -> List[Dict]:
|
||||
"""获取站点的点击记录"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT * FROM ai_mip_click
|
||||
WHERE site_id = ?
|
||||
ORDER BY click_time DESC
|
||||
LIMIT ?
|
||||
""", (site_id, limit))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [self._dict_from_row(row) for row in rows]
|
||||
except Exception as e:
|
||||
logger.error(f"查询点击记录失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_click_count_by_site(self, site_id: int) -> int:
|
||||
"""获取站点的总点击次数"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) as count FROM ai_mip_click WHERE site_id = ?", (site_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
return row['count'] if row else 0
|
||||
except Exception as e:
|
||||
logger.error(f"查询点击次数失败: {str(e)}")
|
||||
return 0
|
||||
|
||||
|
||||
class InteractionManager(DatabaseManager):
|
||||
"""互动记录管理"""
|
||||
|
||||
def record_interaction(self, site_id: int, click_id: int = None,
|
||||
task_id: str = None, interaction_type: str = 'reply',
|
||||
reply_content: str = None, is_successful: bool = False,
|
||||
response_received: bool = False, response_content: str = None,
|
||||
proxy_ip: str = None, fingerprint_id: str = None,
|
||||
error_message: str = None) -> Optional[int]:
|
||||
"""
|
||||
记录一次互动
|
||||
|
||||
Args:
|
||||
site_id: 站点ID
|
||||
click_id: 关联的点击记录ID
|
||||
task_id: 任务ID
|
||||
interaction_type: 互动类型(reply/comment等)
|
||||
reply_content: 回复内容
|
||||
is_successful: 是否成功
|
||||
response_received: 是否收到回复
|
||||
response_content: 对方回复内容
|
||||
proxy_ip: 使用的代理IP
|
||||
fingerprint_id: 浏览器指纹ID
|
||||
error_message: 错误信息
|
||||
|
||||
Returns:
|
||||
互动记录ID
|
||||
"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
ph = self._get_placeholder()
|
||||
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
sql = f"""
|
||||
INSERT INTO ai_mip_interaction (
|
||||
site_id, click_id, task_id, interaction_type,
|
||||
interaction_time, interaction_status, reply_content,
|
||||
execution_mode, browser_type, proxy_ip, fingerprint_id,
|
||||
response_received, response_content, is_successful,
|
||||
error_message, operator
|
||||
) VALUES ({ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph})
|
||||
"""
|
||||
|
||||
cursor.execute(sql, (
|
||||
site_id,
|
||||
click_id,
|
||||
task_id or f'TASK_{datetime.now().strftime("%Y%m%d%H%M%S")}',
|
||||
interaction_type,
|
||||
now,
|
||||
'success' if is_successful else 'failed',
|
||||
reply_content,
|
||||
'auto',
|
||||
'playwright',
|
||||
proxy_ip,
|
||||
fingerprint_id,
|
||||
1 if response_received else 0,
|
||||
response_content,
|
||||
1 if is_successful else 0,
|
||||
error_message,
|
||||
'RPA_SYSTEM'
|
||||
))
|
||||
|
||||
interaction_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# 如果收到回复,更新站点回复次数
|
||||
if response_received:
|
||||
site_mgr = SiteManager()
|
||||
site_mgr.increment_reply_count(site_id)
|
||||
|
||||
logger.info(f"记录互动: site_id={site_id}, interaction_id={interaction_id}, success={is_successful}")
|
||||
return interaction_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"记录互动失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_interactions_by_site(self, site_id: int, limit: int = 100) -> List[Dict]:
|
||||
"""获取站点的互动记录"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT * FROM ai_mip_interaction
|
||||
WHERE site_id = ?
|
||||
ORDER BY interaction_time DESC
|
||||
LIMIT ?
|
||||
""", (site_id, limit))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [self._dict_from_row(row) for row in rows]
|
||||
except Exception as e:
|
||||
logger.error(f"查询互动记录失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_successful_interactions_count(self, site_id: int) -> int:
|
||||
"""获取站点的成功互动次数"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as count FROM ai_mip_interaction
|
||||
WHERE site_id = ? AND is_successful = 1
|
||||
""", (site_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
return row['count'] if row else 0
|
||||
except Exception as e:
|
||||
logger.error(f"查询成功互动次数失败: {str(e)}")
|
||||
return 0
|
||||
|
||||
|
||||
class StatisticsManager(DatabaseManager):
|
||||
"""统计数据管理"""
|
||||
|
||||
def get_statistics(self) -> Dict:
|
||||
"""
|
||||
获取全局统计数据
|
||||
|
||||
Returns:
|
||||
统计数据字典
|
||||
"""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
|
||||
# 站点统计
|
||||
cursor = self._execute_query(conn, "SELECT COUNT(*) as total FROM ai_mip_site")
|
||||
total_sites = cursor.fetchone()['total']
|
||||
|
||||
cursor = self._execute_query(conn, "SELECT COUNT(*) as total FROM ai_mip_site WHERE status = 'active'")
|
||||
active_sites = cursor.fetchone()['total']
|
||||
|
||||
# 点击统计
|
||||
cursor = self._execute_query(conn, "SELECT COUNT(*) as total FROM ai_mip_click")
|
||||
total_clicks = cursor.fetchone()['total']
|
||||
|
||||
# 互动统计
|
||||
cursor = self._execute_query(conn, "SELECT COUNT(*) as total FROM ai_mip_interaction WHERE response_received = 1")
|
||||
total_replies = cursor.fetchone()['total']
|
||||
|
||||
cursor = self._execute_query(conn, "SELECT COUNT(*) as total FROM ai_mip_interaction WHERE is_successful = 1")
|
||||
successful_interactions = cursor.fetchone()['total']
|
||||
|
||||
conn.close()
|
||||
|
||||
reply_rate = (total_replies / total_clicks * 100) if total_clicks > 0 else 0
|
||||
success_rate = (successful_interactions / total_clicks * 100) if total_clicks > 0 else 0
|
||||
|
||||
return {
|
||||
'total_sites': total_sites,
|
||||
'active_sites': active_sites,
|
||||
'total_clicks': total_clicks,
|
||||
'total_replies': total_replies,
|
||||
'successful_interactions': successful_interactions,
|
||||
'reply_rate': f"{reply_rate:.2f}%",
|
||||
'success_rate': f"{success_rate:.2f}%"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取统计数据失败: {str(e)}")
|
||||
return {}
|
||||
|
||||
def get_site_statistics(self, site_id: int) -> Dict:
|
||||
"""
|
||||
获取单个站点的统计数据
|
||||
|
||||
Args:
|
||||
site_id: 站点ID
|
||||
|
||||
Returns:
|
||||
站点统计数据
|
||||
"""
|
||||
try:
|
||||
site_mgr = SiteManager(self.db_path)
|
||||
click_mgr = ClickManager(self.db_path)
|
||||
interaction_mgr = InteractionManager(self.db_path)
|
||||
|
||||
site = site_mgr.get_site_by_id(site_id)
|
||||
if not site:
|
||||
return {}
|
||||
|
||||
click_count = click_mgr.get_click_count_by_site(site_id)
|
||||
success_count = interaction_mgr.get_successful_interactions_count(site_id)
|
||||
|
||||
return {
|
||||
'site_url': site['site_url'],
|
||||
'site_name': site['site_name'],
|
||||
'status': site['status'],
|
||||
'click_count': click_count,
|
||||
'reply_count': site['reply_count'],
|
||||
'successful_interactions': success_count,
|
||||
'reply_rate': f"{(site['reply_count'] / click_count * 100) if click_count > 0 else 0:.2f}%"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取站点统计失败: {str(e)}")
|
||||
return {}
|
||||
BIN
debug_no_input.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
22
open_database_manager.bat
Normal file
@@ -0,0 +1,22 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ====================================
|
||||
echo MIP广告自动化 - 数据库管理页面
|
||||
echo ====================================
|
||||
echo.
|
||||
|
||||
echo 正在启动浏览器打开管理页面...
|
||||
echo.
|
||||
|
||||
REM 检测操作系统并打开浏览器
|
||||
start http://localhost:5000/static/database.html
|
||||
|
||||
echo.
|
||||
echo ✅ 已在浏览器中打开数据库管理页面
|
||||
echo.
|
||||
echo 📌 如果浏览器未自动打开,请手动访问:
|
||||
echo http://localhost:5000/static/database.html
|
||||
echo.
|
||||
echo 💡 提示: 请确保后端服务已启动 (python app.py)
|
||||
echo.
|
||||
pause
|
||||
@@ -1,9 +1,31 @@
|
||||
playwright>=1.35.0
|
||||
requests>=2.28.0
|
||||
urllib3<2.0
|
||||
flask>=2.0.0,<2.3.0
|
||||
flask-cors>=3.0.0
|
||||
apscheduler>=3.9.0
|
||||
python-dotenv>=0.19.0
|
||||
pyyaml>=5.4.0
|
||||
loguru>=0.6.0
|
||||
# MIP广告自动点击系统 - Python依赖包
|
||||
|
||||
# Web框架
|
||||
Flask==3.0.0
|
||||
Werkzeug==3.0.1
|
||||
|
||||
# 浏览器自动化
|
||||
playwright==1.40.0
|
||||
|
||||
# HTTP请求
|
||||
requests==2.31.0
|
||||
|
||||
# 日志处理
|
||||
loguru==0.7.2
|
||||
|
||||
# 任务调度
|
||||
APScheduler==3.10.4
|
||||
|
||||
# 环境变量管理
|
||||
python-dotenv==1.0.0
|
||||
|
||||
# 时区处理
|
||||
pytz==2023.3
|
||||
tzlocal==5.2
|
||||
|
||||
# 数据处理
|
||||
python-dateutil==2.8.2
|
||||
|
||||
# 数据库
|
||||
pymysql==1.1.0
|
||||
cryptography>=41.0.0
|
||||
|
||||
BIN
screenshots/task_1_20260116_150519/01_before_click.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
screenshots/task_1_20260116_150519/02_after_click.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
screenshots/task_1_20260116_150732/01_before_click.png
Normal file
|
After Width: | Height: | Size: 428 KiB |
BIN
screenshots/task_1_20260116_153405/01_before_click.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
screenshots/task_1_20260116_153405/02_after_click.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
screenshots/task_1_20260116_155350/01_before_click.png
Normal file
|
After Width: | Height: | Size: 428 KiB |
BIN
screenshots/task_1_20260116_155637/01_before_click.png
Normal file
|
After Width: | Height: | Size: 438 KiB |
BIN
screenshots/task_1_20260116_155637/02_after_click.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
259
start_production.sh
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MIP广告自动点击系统 - 生产环境启动脚本
|
||||
# 适用于 Ubuntu/Debian 系统
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
echo "============================================================"
|
||||
echo "MIP广告自动点击系统 - 生产环境启动"
|
||||
echo "============================================================"
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 项目目录(脚本所在目录)
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo -e "${GREEN}项目目录: $PROJECT_DIR${NC}"
|
||||
|
||||
# 检查Python版本
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "检查Python环境"
|
||||
echo "============================================================"
|
||||
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo -e "${RED}错误: 未找到 python3${NC}"
|
||||
echo "请安装Python 3.8+: sudo apt-get install python3 python3-pip python3-venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYTHON_VERSION=$(python3 --version | awk '{print $2}')
|
||||
echo -e "${GREEN}Python版本: $PYTHON_VERSION${NC}"
|
||||
|
||||
# 检查Python版本是否 >= 3.8
|
||||
PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1)
|
||||
PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2)
|
||||
|
||||
if [ "$PYTHON_MAJOR" -lt 3 ] || ([ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 8 ]); then
|
||||
echo -e "${RED}错误: Python版本过低,需要 3.8+${NC}"
|
||||
echo "当前版本: $PYTHON_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 虚拟环境目录
|
||||
VENV_DIR="$PROJECT_DIR/venv"
|
||||
|
||||
# 创建虚拟环境(如果不存在)
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "创建Python虚拟环境"
|
||||
echo "============================================================"
|
||||
|
||||
# 检查是否安装了 venv 模块
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
echo -e "${YELLOW}警告: python3-venv 未安装,正在尝试安装...${NC}"
|
||||
|
||||
# 尝试安装(需要sudo权限)
|
||||
if command -v apt-get &> /dev/null; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-venv
|
||||
else
|
||||
echo -e "${RED}错误: 无法自动安装 python3-venv${NC}"
|
||||
echo "请手动执行: sudo apt-get install python3-venv"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}正在创建虚拟环境...${NC}"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
echo -e "${GREEN}✓ 虚拟环境创建成功${NC}"
|
||||
else
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ 虚拟环境已存在: $VENV_DIR${NC}"
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "激活虚拟环境"
|
||||
echo "============================================================"
|
||||
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
if [ "$VIRTUAL_ENV" != "" ]; then
|
||||
echo -e "${GREEN}✓ 虚拟环境已激活: $VIRTUAL_ENV${NC}"
|
||||
echo -e "${GREEN}Python路径: $(which python)${NC}"
|
||||
else
|
||||
echo -e "${RED}错误: 虚拟环境激活失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 升级pip
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "升级pip"
|
||||
echo "============================================================"
|
||||
python -m pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# 安装依赖
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "安装项目依赖"
|
||||
echo "============================================================"
|
||||
|
||||
if [ -f "requirements.txt" ]; then
|
||||
echo -e "${GREEN}从 requirements.txt 安装依赖...${NC}"
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
else
|
||||
echo -e "${YELLOW}警告: requirements.txt 不存在${NC}"
|
||||
echo "手动安装核心依赖..."
|
||||
pip install flask playwright requests loguru apscheduler python-dotenv -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# 安装Playwright浏览器
|
||||
echo "安装Playwright浏览器驱动..."
|
||||
playwright install chromium
|
||||
fi
|
||||
|
||||
# 检查配置文件
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "检查配置文件"
|
||||
echo "============================================================"
|
||||
|
||||
if [ ! -f ".env.production" ]; then
|
||||
echo -e "${YELLOW}警告: .env.production 不存在${NC}"
|
||||
|
||||
if [ -f ".env.example" ]; then
|
||||
echo "从 .env.example 创建配置文件..."
|
||||
cp .env.example .env.production
|
||||
echo -e "${GREEN}✓ 已创建 .env.production${NC}"
|
||||
echo -e "${RED}请编辑 .env.production 配置文件后重新启动${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${RED}错误: 缺少配置文件模板${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}✓ 配置文件存在: .env.production${NC}"
|
||||
fi
|
||||
|
||||
# 初始化数据库
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "初始化数据库"
|
||||
echo "============================================================"
|
||||
|
||||
if [ ! -f "db/ai_mip_prod.db" ]; then
|
||||
echo -e "${YELLOW}数据库不存在,正在初始化...${NC}"
|
||||
|
||||
if [ -f "db/init_databases.py" ]; then
|
||||
# 自动创建生产数据库(跳过交互)
|
||||
python << EOF
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
db_dir = Path('db')
|
||||
db_path = db_dir / 'ai_mip_prod.db'
|
||||
|
||||
if not db_path.exists():
|
||||
print(f"创建数据库: {db_path}")
|
||||
|
||||
# 读取并执行SQL脚本
|
||||
init_sql = db_dir / 'init_sqlite.sql'
|
||||
if init_sql.exists():
|
||||
with open(init_sql, 'r', encoding='utf-8') as f:
|
||||
sql_script = f.read()
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.executescript(sql_script)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("✓ 数据库初始化完成")
|
||||
else:
|
||||
print("错误: 找不到 init_sqlite.sql")
|
||||
exit(1)
|
||||
EOF
|
||||
echo -e "${GREEN}✓ 数据库初始化成功${NC}"
|
||||
else
|
||||
echo -e "${RED}错误: 找不到数据库初始化脚本${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}✓ 数据库已存在: db/ai_mip_prod.db${NC}"
|
||||
fi
|
||||
|
||||
# 创建必要的目录
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "创建必要目录"
|
||||
echo "============================================================"
|
||||
|
||||
mkdir -p logs data
|
||||
echo -e "${GREEN}✓ 目录创建完成${NC}"
|
||||
|
||||
# 检查AdsPower连接
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "检查AdsPower连接"
|
||||
echo "============================================================"
|
||||
|
||||
python << EOF
|
||||
import os
|
||||
os.environ['ENV'] = 'production'
|
||||
|
||||
try:
|
||||
from adspower_client import AdsPowerClient
|
||||
client = AdsPowerClient()
|
||||
profiles = client.list_profiles()
|
||||
|
||||
if profiles:
|
||||
print("\033[0;32m✓ AdsPower连接正常\033[0m")
|
||||
profile_count = len(profiles.get('data', {}).get('list', []))
|
||||
print(f"\033[0;32m Profile数量: {profile_count}\033[0m")
|
||||
else:
|
||||
print("\033[1;33m警告: AdsPower连接失败,请检查配置\033[0m")
|
||||
except Exception as e:
|
||||
print(f"\033[1;33m警告: AdsPower连接异常: {str(e)}\033[0m")
|
||||
print("\033[1;33m 请确保AdsPower客户端已启动\033[0m")
|
||||
EOF
|
||||
|
||||
# 启动服务
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "启动Flask服务"
|
||||
echo "============================================================"
|
||||
|
||||
export ENV=production
|
||||
|
||||
# 检查端口是否被占用
|
||||
PORT=5000
|
||||
if command -v netstat &> /dev/null; then
|
||||
if netstat -tuln | grep ":$PORT " > /dev/null; then
|
||||
echo -e "${YELLOW}警告: 端口 $PORT 已被占用${NC}"
|
||||
echo "尝试查找占用进程..."
|
||||
lsof -i :$PORT || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}启动服务中...${NC}"
|
||||
echo "访问地址: http://127.0.0.1:5000"
|
||||
echo "按 Ctrl+C 停止服务"
|
||||
echo ""
|
||||
|
||||
# 使用nohup在后台运行(可选)
|
||||
# nohup python app.py > logs/app.log 2>&1 &
|
||||
# echo $! > app.pid
|
||||
# echo -e "${GREEN}✓ 服务已启动(后台运行)${NC}"
|
||||
# echo "PID: $(cat app.pid)"
|
||||
# echo "日志: logs/app.log"
|
||||
|
||||
# 前台运行(便于调试)
|
||||
python app.py
|
||||
875
static/app.html
Normal file
@@ -0,0 +1,875 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MIP广告自动化管理系统</title>
|
||||
<!-- Element UI CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
<!-- Vue -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
|
||||
<!-- Element UI JS -->
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
<!-- Axios -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.top-header {
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #52c41a;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.main-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
/* 左侧菜单 */
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
background: #001529;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-card.primary .stat-value { color: #1890ff; }
|
||||
.stat-card.success .stat-value { color: #52c41a; }
|
||||
.stat-card.warning .stat-value { color: #faad14; }
|
||||
.stat-card.danger .stat-value { color: #f5222d; }
|
||||
|
||||
/* 内容卡片 */
|
||||
.content-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-table th {
|
||||
background: #fafafa;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* URL链接 */
|
||||
.url-link {
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
max-width: 400px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.url-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* 数据库标签页 */
|
||||
.db-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="top-header">
|
||||
<div class="logo-section">
|
||||
<div class="logo-icon">📊</div>
|
||||
<div class="logo-text">MIP广告自动化管理系统</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="status-badge">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ schedulerStatus }}</span>
|
||||
</div>
|
||||
<el-button type="text" style="color: white;">
|
||||
<i class="el-icon-user"></i> 管理员
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主容器 -->
|
||||
<div class="main-container">
|
||||
<!-- 左侧菜单 -->
|
||||
<div class="sidebar">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="el-menu-vertical"
|
||||
background-color="#001529"
|
||||
text-color="#fff"
|
||||
active-text-color="#1890ff"
|
||||
@select="handleMenuSelect">
|
||||
<el-menu-item index="dashboard">
|
||||
<i class="el-icon-data-line"></i>
|
||||
<span slot="title">数据概览</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="scheduler">
|
||||
<i class="el-icon-setting"></i>
|
||||
<span slot="title">调度器管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="urls">
|
||||
<i class="el-icon-link"></i>
|
||||
<span slot="title">链接管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="database">
|
||||
<i class="el-icon-coin"></i>
|
||||
<span slot="title">数据库管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="browser">
|
||||
<i class="el-icon-monitor"></i>
|
||||
<span slot="title">浏览器测试</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="content-area">
|
||||
<!-- 数据概览 -->
|
||||
<div v-show="activeMenu === 'dashboard'">
|
||||
<el-row :gutter="24" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-label">总链接数</div>
|
||||
<div class="stat-value">{{ stats.totalUrls }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card success">
|
||||
<div class="stat-label">总点击次数</div>
|
||||
<div class="stat-value">{{ stats.totalClicks }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-label">获得回复</div>
|
||||
<div class="stat-value">{{ stats.totalReplies }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card danger">
|
||||
<div class="stat-label">回复率</div>
|
||||
<div class="stat-value">{{ stats.replyRate }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">链接列表</div>
|
||||
<el-button type="primary" size="small" icon="el-icon-refresh" @click="loadUrlList">刷新</el-button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-table :data="urlList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="site_url" label="链接地址" min-width="300">
|
||||
<template slot-scope="scope">
|
||||
<a :href="scope.row.site_url" target="_blank" class="url-link" :title="scope.row.site_url">
|
||||
{{ scope.row.site_url }}
|
||||
</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="click_count" label="点击次数" width="120" align="center"></el-table-column>
|
||||
<el-table-column prop="reply_count" label="回复次数" width="120" align="center"></el-table-column>
|
||||
<el-table-column prop="last_click_time" label="上次点击时间" width="180"></el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="warning" @click="resetUrl(scope.row.site_url)">重置</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteUrl(scope.row.site_url)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 调度器管理 -->
|
||||
<div v-show="activeMenu === 'scheduler'">
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">调度器控制</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-button type="success" icon="el-icon-video-play" @click="startScheduler">启动调度器</el-button>
|
||||
<el-button type="danger" icon="el-icon-video-pause" @click="stopScheduler">停止调度器</el-button>
|
||||
|
||||
<el-alert
|
||||
title="调度规则说明"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-top: 24px;">
|
||||
<ul style="padding-left: 20px; line-height: 2;">
|
||||
<li>每30分钟点击一次添加的链接</li>
|
||||
<li>仅在09:00-21:00时间段执行</li>
|
||||
<li>每个链接随机点击1-10次</li>
|
||||
<li>等待最多30秒查看回复</li>
|
||||
</ul>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 链接管理 -->
|
||||
<div v-show="activeMenu === 'urls'">
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">添加单个链接</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-input
|
||||
v-model="singleUrl"
|
||||
placeholder="请输入MIP页面链接"
|
||||
clearable>
|
||||
</el-input>
|
||||
<el-button type="primary" style="margin-top: 16px;" @click="addSingleUrl">添加链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">批量添加链接</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="batchUrls"
|
||||
:rows="8"
|
||||
placeholder="每行输入一个链接,支持批量添加">
|
||||
</el-input>
|
||||
<el-button type="primary" style="margin-top: 16px;" @click="addBatchUrls">批量添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据库管理 -->
|
||||
<div v-show="activeMenu === 'database'">
|
||||
<el-row :gutter="24" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-label">总站点数</div>
|
||||
<div class="stat-value">{{ dbStats.totalSites }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card success">
|
||||
<div class="stat-label">总点击数</div>
|
||||
<div class="stat-value">{{ dbStats.totalClicks }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-label">总回复数</div>
|
||||
<div class="stat-value">{{ dbStats.totalReplies }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card danger">
|
||||
<div class="stat-label">成功互动</div>
|
||||
<div class="stat-value">{{ dbStats.successful }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">数据库记录</div>
|
||||
<el-button type="primary" size="small" icon="el-icon-refresh" @click="loadDatabaseData">刷新</el-button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-tabs v-model="dbActiveTab" @tab-click="handleDbTabClick">
|
||||
<el-tab-pane label="站点列表" name="sites">
|
||||
<el-table :data="dbSites" style="width: 100%" v-loading="dbLoading">
|
||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||
<el-table-column prop="site_name" label="站点名称" width="150"></el-table-column>
|
||||
<el-table-column prop="site_url" label="URL" min-width="250">
|
||||
<template slot-scope="scope">
|
||||
<a :href="scope.row.site_url" target="_blank" class="url-link" :title="scope.row.site_url">
|
||||
{{ scope.row.site_url }}
|
||||
</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.status === 'active' ? '活跃' : '未激活' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="click_count" label="点击数" width="100" align="center"></el-table-column>
|
||||
<el-table-column prop="reply_count" label="回复数" width="100" align="center"></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="点击记录" name="clicks">
|
||||
<el-table :data="dbClicks" style="width: 100%" v-loading="dbLoading">
|
||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||
<el-table-column prop="site_id" label="站点ID" width="100"></el-table-column>
|
||||
<el-table-column prop="site_url" label="URL" min-width="300">
|
||||
<template slot-scope="scope">
|
||||
<span :title="scope.row.site_url">{{ scope.row.site_url }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="click_time" label="点击时间" width="180"></el-table-column>
|
||||
<el-table-column prop="device_type" label="设备类型" width="100" align="center"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="互动记录" name="interactions">
|
||||
<el-table :data="dbInteractions" style="width: 100%" v-loading="dbLoading">
|
||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||
<el-table-column prop="site_id" label="站点ID" width="100"></el-table-column>
|
||||
<el-table-column prop="interaction_time" label="互动时间" width="180"></el-table-column>
|
||||
<el-table-column prop="reply_content" label="发送内容" min-width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip :content="scope.row.reply_content" placement="top">
|
||||
<span>{{ scope.row.reply_content ? scope.row.reply_content.substring(0, 30) + '...' : '-' }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="response_received" label="收到回复" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.response_received ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.response_received ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="interaction_status" label="状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.interaction_status === 'success' ? 'success' : 'danger'" size="small">
|
||||
{{ scope.row.interaction_status || '-' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 浏览器测试 -->
|
||||
<div v-show="activeMenu === 'browser'">
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">AdsPower浏览器测试</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<el-alert
|
||||
title="此功能用于测试AdsPower浏览器连接是否正常"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 24px;">
|
||||
</el-alert>
|
||||
|
||||
<el-button type="primary" @click="testBrowser(false)">测试浏览器(不使用代理)</el-button>
|
||||
<el-button type="warning" @click="testBrowser(true)">测试浏览器(使用代理)</el-button>
|
||||
|
||||
<div v-if="browserTestResult" style="margin-top: 24px;">
|
||||
<el-alert :title="browserTestResult" type="info" :closable="false"></el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
activeMenu: 'dashboard',
|
||||
schedulerStatus: '系统运行中',
|
||||
loading: false,
|
||||
dbLoading: false,
|
||||
|
||||
// 统计数据
|
||||
stats: {
|
||||
totalUrls: 0,
|
||||
totalClicks: 0,
|
||||
totalReplies: 0,
|
||||
replyRate: '0%'
|
||||
},
|
||||
|
||||
// URL列表
|
||||
urlList: [],
|
||||
|
||||
// 表单
|
||||
singleUrl: '',
|
||||
batchUrls: '',
|
||||
|
||||
// 数据库统计
|
||||
dbStats: {
|
||||
totalSites: 0,
|
||||
totalClicks: 0,
|
||||
totalReplies: 0,
|
||||
successful: 0
|
||||
},
|
||||
|
||||
// 数据库数据
|
||||
dbActiveTab: 'sites',
|
||||
dbSites: [],
|
||||
dbClicks: [],
|
||||
dbInteractions: [],
|
||||
|
||||
// 浏览器测试
|
||||
browserTestResult: '',
|
||||
|
||||
// API地址
|
||||
apiBase: 'http://127.0.0.1:5000'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
await this.getSchedulerStatus();
|
||||
await this.getStatistics();
|
||||
await this.loadUrlList();
|
||||
|
||||
// 定时刷新
|
||||
setInterval(() => {
|
||||
this.getSchedulerStatus();
|
||||
this.getStatistics();
|
||||
if (this.activeMenu === 'dashboard') {
|
||||
this.loadUrlList();
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
handleMenuSelect(index) {
|
||||
this.activeMenu = index;
|
||||
if (index === 'database') {
|
||||
this.loadDatabaseData();
|
||||
}
|
||||
},
|
||||
|
||||
// 获取调度器状态
|
||||
async getSchedulerStatus() {
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/scheduler/status`);
|
||||
if (data.success) {
|
||||
this.schedulerStatus = data.data.status === 'running' ? '调度器运行中' : '调度器已停止';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取调度器状态失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取统计数据
|
||||
async getStatistics() {
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/statistics`);
|
||||
if (data.success) {
|
||||
this.stats = {
|
||||
totalUrls: data.data.total_sites || 0,
|
||||
totalClicks: data.data.total_clicks || 0,
|
||||
totalReplies: data.data.total_replies || 0,
|
||||
replyRate: data.data.reply_rate || '0%'
|
||||
};
|
||||
|
||||
this.dbStats = {
|
||||
totalSites: data.data.total_sites || 0,
|
||||
totalClicks: data.data.total_clicks || 0,
|
||||
totalReplies: data.data.total_replies || 0,
|
||||
successful: data.data.successful_interactions || 0
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载URL列表
|
||||
async loadUrlList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/urls`);
|
||||
if (data.success) {
|
||||
this.urlList = data.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('加载URL列表失败');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 启动调度器
|
||||
async startScheduler() {
|
||||
try {
|
||||
const { data } = await axios.post(`${this.apiBase}/api/scheduler/start`);
|
||||
if (data.success) {
|
||||
this.$message.success('调度器已启动');
|
||||
this.getSchedulerStatus();
|
||||
} else {
|
||||
this.$message.error(data.message || '启动失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('启动失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// 停止调度器
|
||||
async stopScheduler() {
|
||||
try {
|
||||
const { data } = await axios.post(`${this.apiBase}/api/scheduler/stop`);
|
||||
if (data.success) {
|
||||
this.$message.success('调度器已停止');
|
||||
this.getSchedulerStatus();
|
||||
} else {
|
||||
this.$message.error(data.message || '停止失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('停止失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// 添加单个URL
|
||||
async addSingleUrl() {
|
||||
if (!this.singleUrl.trim()) {
|
||||
this.$message.warning('请输入链接');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(`${this.apiBase}/api/urls`, {
|
||||
url: this.singleUrl
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
this.$message.success('添加成功');
|
||||
this.singleUrl = '';
|
||||
this.loadUrlList();
|
||||
this.getStatistics();
|
||||
} else {
|
||||
this.$message.error(data.message || '添加失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('添加失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// 批量添加URL
|
||||
async addBatchUrls() {
|
||||
if (!this.batchUrls.trim()) {
|
||||
this.$message.warning('请输入链接');
|
||||
return;
|
||||
}
|
||||
|
||||
const urls = this.batchUrls.split('\n').map(u => u.trim()).filter(u => u);
|
||||
if (urls.length === 0) {
|
||||
this.$message.warning('请输入有效链接');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(`${this.apiBase}/api/urls`, { urls });
|
||||
|
||||
if (data.success) {
|
||||
this.$message.success(`成功添加 ${data.added_count}/${data.total_count} 个链接`);
|
||||
this.batchUrls = '';
|
||||
this.loadUrlList();
|
||||
this.getStatistics();
|
||||
} else {
|
||||
this.$message.error(data.message || '添加失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('添加失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// 重置URL
|
||||
async resetUrl(url) {
|
||||
try {
|
||||
await this.$confirm('确定要重置该链接吗?', '提示', {
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
const { data } = await axios.post(`${this.apiBase}/api/urls/${encodeURIComponent(url)}/reset`);
|
||||
|
||||
if (data.success) {
|
||||
this.$message.success('重置成功');
|
||||
this.loadUrlList();
|
||||
this.getStatistics();
|
||||
} else {
|
||||
this.$message.error(data.message || '重置失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
this.$message.error('重置失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 删除URL
|
||||
async deleteUrl(url) {
|
||||
try {
|
||||
await this.$confirm('确定要删除该链接吗?', '提示', {
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
const { data } = await axios.delete(`${this.apiBase}/api/urls/${encodeURIComponent(url)}`);
|
||||
|
||||
if (data.success) {
|
||||
this.$message.success('删除成功');
|
||||
this.loadUrlList();
|
||||
this.getStatistics();
|
||||
} else {
|
||||
this.$message.error(data.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
this.$message.error('删除失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 加载数据库数据
|
||||
async loadDatabaseData() {
|
||||
this.dbLoading = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadDbSites(),
|
||||
this.loadDbClicks(),
|
||||
this.loadDbInteractions()
|
||||
]);
|
||||
} finally {
|
||||
this.dbLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadDbSites() {
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/urls`);
|
||||
if (data.success) {
|
||||
this.dbSites = data.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载站点数据失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadDbClicks() {
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/clicks?limit=100`);
|
||||
if (data.success) {
|
||||
this.dbClicks = data.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载点击记录失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadDbInteractions() {
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/interactions?limit=100`);
|
||||
if (data.success) {
|
||||
this.dbInteractions = data.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载互动记录失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
handleDbTabClick() {
|
||||
// 标签页切换时可以重新加载数据
|
||||
},
|
||||
|
||||
// 浏览器测试
|
||||
testBrowser(useProxy) {
|
||||
this.browserTestResult = '浏览器测试功能待开发,请使用命令行运行: python test_adspower_playwright.py';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -24,6 +24,10 @@
|
||||
<span class="menu-icon">🔗</span>
|
||||
<span>链接管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="location.href='database.html'">
|
||||
<span class="menu-icon">💾</span>
|
||||
<span>数据库管理</span>
|
||||
</div>
|
||||
<div class="menu-item active" onclick="location.href='browser.html'">
|
||||
<span class="menu-icon">🌐</span>
|
||||
<span>浏览器测试</span>
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
<span class="menu-icon">🔗</span>
|
||||
<span>链接管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="location.href='database.html'">
|
||||
<span class="menu-icon">💾</span>
|
||||
<span>数据库管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="location.href='browser.html'">
|
||||
<span class="menu-icon">🌐</span>
|
||||
<span>浏览器测试</span>
|
||||
|
||||
909
static/database.html
Normal file
@@ -0,0 +1,909 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数据库管理 - MIP广告自动化</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background: #f0f2f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 左侧菜单栏 */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 200px;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
|
||||
color: white;
|
||||
padding: 20px 0;
|
||||
box-shadow: 2px 0 8px rgba(0,0,0,0.1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
padding: 0 20px 30px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logo p {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-left: 3px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-left-color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
.menu-back {
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-left: 3px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.menu-back:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-left-color: rgba(255,255,255,0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
margin-left: 200px;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #1890ff;
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.stat-card-title {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-card-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-card-trend {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.stat-card.primary .stat-card-value { color: #1890ff; }
|
||||
.stat-card.success .stat-card-value { color: #52c41a; }
|
||||
.stat-card.warning .stat-card-value { color: #faad14; }
|
||||
.stat-card.info .stat-card-value { color: #13c2c2; }
|
||||
|
||||
/* 内容卡片 */
|
||||
.content-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #52c41a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #73d13d;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #ff7875;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
border-bottom: 2px solid #f0f2f5;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 3px solid #f0f2f5;
|
||||
border-top: 3px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Toast提示 */
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: white;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
min-width: 300px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.success { border-left: 4px solid #52c41a; }
|
||||
.toast.error { border-left: 4px solid #ff4d4f; }
|
||||
.toast.info { border-left: 4px solid #1890ff; }
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 文本换行 */
|
||||
.text-wrap {
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.page-btn:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 左侧菜单 -->
|
||||
<div class="sidebar">
|
||||
<div class="logo">
|
||||
<h2>📊 数据库管理</h2>
|
||||
<p>MIP广告自动化系统</p>
|
||||
</div>
|
||||
<div class="menu-back" onclick="window.location.href='index.html'">
|
||||
<span class="menu-item-icon">🏠</span>
|
||||
<span>返回主页</span>
|
||||
</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-item active" data-view="overview">
|
||||
<span class="menu-item-icon">📈</span>
|
||||
<span>数据概览</span>
|
||||
</div>
|
||||
<div class="menu-item" data-view="sites">
|
||||
<span class="menu-item-icon">🌐</span>
|
||||
<span>站点管理</span>
|
||||
</div>
|
||||
<div class="menu-item" data-view="clicks">
|
||||
<span class="menu-item-icon">👆</span>
|
||||
<span>点击记录</span>
|
||||
</div>
|
||||
<div class="menu-item" data-view="interactions">
|
||||
<span class="menu-item-icon">💬</span>
|
||||
<span>互动记录</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 数据概览视图 -->
|
||||
<div id="overview-view" class="view-content">
|
||||
<div class="header">
|
||||
<h1>数据概览</h1>
|
||||
<p>实时查看系统运行统计数据</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-card-title">总站点数</div>
|
||||
<div class="stat-card-value" id="stat-total-sites">-</div>
|
||||
<div class="stat-card-trend">活跃: <span id="stat-active-sites">-</span></div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-card-title">总点击数</div>
|
||||
<div class="stat-card-value" id="stat-total-clicks">-</div>
|
||||
<div class="stat-card-trend">今日: <span id="stat-today-clicks">-</span></div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-card-title">总回复数</div>
|
||||
<div class="stat-card-value" id="stat-total-replies">-</div>
|
||||
<div class="stat-card-trend">回复率: <span id="stat-reply-rate">-</span></div>
|
||||
</div>
|
||||
<div class="stat-card info">
|
||||
<div class="stat-card-title">成功互动</div>
|
||||
<div class="stat-card-value" id="stat-successful">-</div>
|
||||
<div class="stat-card-trend">成功率: <span id="stat-success-rate">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近的站点 -->
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">最近的站点</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-primary" onclick="refreshData()">🔄 刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>站点名称</th>
|
||||
<th>URL</th>
|
||||
<th>状态</th>
|
||||
<th>点击数</th>
|
||||
<th>回复数</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recent-sites-table">
|
||||
<tr>
|
||||
<td colspan="7" class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>加载中...</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 站点管理视图 -->
|
||||
<div id="sites-view" class="view-content" style="display: none;">
|
||||
<div class="header">
|
||||
<h1>站点管理</h1>
|
||||
<p>管理所有MIP站点信息</p>
|
||||
</div>
|
||||
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">站点列表</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-primary" onclick="refreshSites()">🔄 刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>站点名称</th>
|
||||
<th>URL</th>
|
||||
<th>维度</th>
|
||||
<th>状态</th>
|
||||
<th>点击/回复</th>
|
||||
<th>频次</th>
|
||||
<th>时间段</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sites-table">
|
||||
<tr>
|
||||
<td colspan="9" class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>加载中...</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 点击记录视图 -->
|
||||
<div id="clicks-view" class="view-content" style="display: none;">
|
||||
<div class="header">
|
||||
<h1>点击记录</h1>
|
||||
<p>查看所有广告点击记录</p>
|
||||
</div>
|
||||
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">点击记录列表</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-primary" onclick="refreshClicks()">🔄 刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>站点ID</th>
|
||||
<th>站点URL</th>
|
||||
<th>点击时间</th>
|
||||
<th>用户IP</th>
|
||||
<th>设备类型</th>
|
||||
<th>任务ID</th>
|
||||
<th>操作者</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="clicks-table">
|
||||
<tr>
|
||||
<td colspan="8" class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>加载中...</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 互动记录视图 -->
|
||||
<div id="interactions-view" class="view-content" style="display: none;">
|
||||
<div class="header">
|
||||
<h1>互动记录</h1>
|
||||
<p>查看所有咨询互动记录</p>
|
||||
</div>
|
||||
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">互动记录列表</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-primary" onclick="refreshInteractions()">🔄 刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>站点ID</th>
|
||||
<th>点击ID</th>
|
||||
<th>互动时间</th>
|
||||
<th>互动类型</th>
|
||||
<th>发送内容</th>
|
||||
<th>收到回复</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="interactions-table">
|
||||
<tr>
|
||||
<td colspan="8" class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>加载中...</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast提示 -->
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:5000';
|
||||
|
||||
// Toast提示
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast ${type}`;
|
||||
toast.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 菜单切换
|
||||
document.querySelectorAll('.menu-item').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
const view = this.dataset.view;
|
||||
|
||||
// 更新菜单激活状态
|
||||
document.querySelectorAll('.menu-item').forEach(i => i.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// 切换视图
|
||||
document.querySelectorAll('.view-content').forEach(v => v.style.display = 'none');
|
||||
document.getElementById(`${view}-view`).style.display = 'block';
|
||||
|
||||
// 加载对应数据
|
||||
switch(view) {
|
||||
case 'overview':
|
||||
loadOverview();
|
||||
break;
|
||||
case 'sites':
|
||||
loadSites();
|
||||
break;
|
||||
case 'clicks':
|
||||
loadClicks();
|
||||
break;
|
||||
case 'interactions':
|
||||
loadInteractions();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 加载概览数据
|
||||
async function loadOverview() {
|
||||
try {
|
||||
// 加载统计数据
|
||||
const statsRes = await fetch(`${API_BASE}/api/statistics`);
|
||||
const statsData = await statsRes.json();
|
||||
|
||||
if (statsData.success) {
|
||||
const stats = statsData.data;
|
||||
document.getElementById('stat-total-sites').textContent = stats.total_sites || 0;
|
||||
document.getElementById('stat-active-sites').textContent = stats.active_sites || 0;
|
||||
document.getElementById('stat-total-clicks').textContent = stats.total_clicks || 0;
|
||||
document.getElementById('stat-today-clicks').textContent = '-';
|
||||
document.getElementById('stat-total-replies').textContent = stats.total_replies || 0;
|
||||
document.getElementById('stat-reply-rate').textContent = stats.reply_rate || '0%';
|
||||
document.getElementById('stat-successful').textContent = stats.successful_interactions || 0;
|
||||
document.getElementById('stat-success-rate').textContent = stats.success_rate || '0%';
|
||||
}
|
||||
|
||||
// 加载最近站点
|
||||
const sitesRes = await fetch(`${API_BASE}/api/urls`);
|
||||
const sitesData = await sitesRes.json();
|
||||
|
||||
const tbody = document.getElementById('recent-sites-table');
|
||||
if (sitesData.success && sitesData.data && sitesData.data.length > 0) {
|
||||
tbody.innerHTML = sitesData.data.slice(0, 10).map(site => `
|
||||
<tr>
|
||||
<td>${site.id}</td>
|
||||
<td>${site.site_name || '-'}</td>
|
||||
<td><span class="text-ellipsis" title="${site.site_url}">${site.site_url}</span></td>
|
||||
<td><span class="status-badge status-${site.status === 'active' ? 'active' : 'inactive'}">${site.status === 'active' ? '活跃' : '未激活'}</span></td>
|
||||
<td>${site.click_count || 0}</td>
|
||||
<td>${site.reply_count || 0}</td>
|
||||
<td>${site.created_at || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div>暂无数据</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('加载数据失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载站点数据
|
||||
async function loadSites() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/urls`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('sites-table');
|
||||
if (data.success && data.data && data.data.length > 0) {
|
||||
tbody.innerHTML = data.data.map(site => `
|
||||
<tr>
|
||||
<td>${site.id}</td>
|
||||
<td>${site.site_name || '-'}</td>
|
||||
<td><span class="text-ellipsis" title="${site.site_url}">${site.site_url}</span></td>
|
||||
<td>${site.site_dimension || '-'}</td>
|
||||
<td><span class="status-badge status-${site.status === 'active' ? 'active' : 'inactive'}">${site.status === 'active' ? '活跃' : '未激活'}</span></td>
|
||||
<td>${site.click_count || 0} / ${site.reply_count || 0}</td>
|
||||
<td>${site.frequency || '-'}</td>
|
||||
<td>${site.time_start || '-'} ~ ${site.time_end || '-'}</td>
|
||||
<td>${site.created_at || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="9" class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div>暂无站点数据</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('加载站点数据失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载点击记录
|
||||
async function loadClicks() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/clicks?limit=100`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('clicks-table');
|
||||
if (data.success && data.data && data.data.length > 0) {
|
||||
tbody.innerHTML = data.data.map(click => `
|
||||
<tr>
|
||||
<td>${click.id}</td>
|
||||
<td>${click.site_id || '-'}</td>
|
||||
<td><span class="text-ellipsis" title="${click.site_url || ''}">${click.site_url || '-'}</span></td>
|
||||
<td>${click.click_time || '-'}</td>
|
||||
<td>${click.user_ip || '-'}</td>
|
||||
<td>${click.device_type || '-'}</td>
|
||||
<td>${click.task_id || '-'}</td>
|
||||
<td>${click.operator || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div>暂无点击记录</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
const tbody = document.getElementById('clicks-table');
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">
|
||||
<div class="empty-state-icon">❌</div>
|
||||
<div>加载失败: ${error.message}</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
showToast('加载点击记录失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载互动记录
|
||||
async function loadInteractions() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/interactions?limit=100`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('interactions-table');
|
||||
if (data.success && data.data && data.data.length > 0) {
|
||||
tbody.innerHTML = data.data.map(interaction => `
|
||||
<tr>
|
||||
<td>${interaction.id}</td>
|
||||
<td>${interaction.site_id || '-'}</td>
|
||||
<td>${interaction.click_id || '-'}</td>
|
||||
<td>${interaction.interaction_time || '-'}</td>
|
||||
<td>${interaction.interaction_type || '-'}</td>
|
||||
<td><span class="text-ellipsis" title="${interaction.reply_content || ''}">${interaction.reply_content || '-'}</span></td>
|
||||
<td><span class="status-badge ${interaction.response_received ? 'status-success' : 'status-inactive'}">${interaction.response_received ? '是' : '否'}</span></td>
|
||||
<td><span class="status-badge status-${interaction.interaction_status === 'success' ? 'success' : 'failed'}">${interaction.interaction_status || '-'}</span></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div>暂无互动记录</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
const tbody = document.getElementById('interactions-table');
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">
|
||||
<div class="empty-state-icon">❌</div>
|
||||
<div>加载失败: ${error.message}</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
showToast('加载互动记录失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新函数
|
||||
function refreshData() {
|
||||
showToast('正在刷新数据...', 'info');
|
||||
loadOverview();
|
||||
}
|
||||
|
||||
function refreshSites() {
|
||||
showToast('正在刷新站点数据...', 'info');
|
||||
loadSites();
|
||||
}
|
||||
|
||||
function refreshClicks() {
|
||||
showToast('正在刷新点击记录...', 'info');
|
||||
loadClicks();
|
||||
}
|
||||
|
||||
function refreshInteractions() {
|
||||
showToast('正在刷新互动记录...', 'info');
|
||||
loadInteractions();
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
window.onload = function() {
|
||||
loadOverview();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -414,6 +414,10 @@
|
||||
<span class="menu-icon">🔗</span>
|
||||
<span>链接管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="switchPage('database')">
|
||||
<span class="menu-icon">💾</span>
|
||||
<span>数据库管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="switchPage('browser')">
|
||||
<span class="menu-icon">🌐</span>
|
||||
<span>浏览器测试</span>
|
||||
@@ -543,6 +547,111 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据库管理页面 -->
|
||||
<div id="database" class="page-content">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid" style="margin-bottom: 24px;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总站点数</div>
|
||||
<div class="stat-value" id="dbTotalSites">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总点击数</div>
|
||||
<div class="stat-value" id="dbTotalClicks">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总回复数</div>
|
||||
<div class="stat-value" id="dbTotalReplies">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">成功互动</div>
|
||||
<div class="stat-value" id="dbSuccessful">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表切换 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<button class="btn btn-primary" id="btnShowSites" onclick="showDatabaseView('sites')">站点列表</button>
|
||||
<button class="btn" id="btnShowClicks" onclick="showDatabaseView('clicks')">点击记录</button>
|
||||
<button class="btn" id="btnShowInteractions" onclick="showDatabaseView('interactions')">互动记录</button>
|
||||
<button class="btn btn-success" onclick="refreshDatabaseData()" style="margin-left: auto;">🔄 刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 站点列表 -->
|
||||
<div id="dbSitesView" class="db-view">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>站点名称</th>
|
||||
<th>URL</th>
|
||||
<th>状态</th>
|
||||
<th>点击数</th>
|
||||
<th>回复数</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dbSitesTableBody">
|
||||
<tr>
|
||||
<td colspan="7" class="empty-state">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 点击记录 -->
|
||||
<div id="dbClicksView" class="db-view" style="display: none;">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>站点ID</th>
|
||||
<th>URL</th>
|
||||
<th>点击时间</th>
|
||||
<th>设备类型</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dbClicksTableBody">
|
||||
<tr>
|
||||
<td colspan="5" class="empty-state">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 互动记录 -->
|
||||
<div id="dbInteractionsView" class="db-view" style="display: none;">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>站点ID</th>
|
||||
<th>互动时间</th>
|
||||
<th>发送内容</th>
|
||||
<th>收到回复</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dbInteractionsTableBody">
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -565,9 +674,15 @@
|
||||
'dashboard': '数据概览',
|
||||
'scheduler': '调度器管理',
|
||||
'urls': '链接管理',
|
||||
'database': '数据库管理',
|
||||
'browser': '浏览器测试'
|
||||
};
|
||||
document.getElementById('breadcrumb').textContent = breadcrumbMap[page];
|
||||
|
||||
// 如果切换到数据库页面,加载数据
|
||||
if (page === 'database') {
|
||||
loadDatabaseData();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示Toast提示
|
||||
@@ -822,6 +937,130 @@
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ==================== 数据库管理相关函数 ====================
|
||||
|
||||
// 切换数据库视图
|
||||
function showDatabaseView(view) {
|
||||
// 隐藏所有视图
|
||||
document.querySelectorAll('.db-view').forEach(v => v.style.display = 'none');
|
||||
|
||||
// 显示目标视图
|
||||
document.getElementById(`db${view.charAt(0).toUpperCase() + view.slice(1)}View`).style.display = 'block';
|
||||
|
||||
// 更新按钮状态
|
||||
document.getElementById('btnShowSites').className = view === 'sites' ? 'btn btn-primary' : 'btn';
|
||||
document.getElementById('btnShowClicks').className = view === 'clicks' ? 'btn btn-primary' : 'btn';
|
||||
document.getElementById('btnShowInteractions').className = view === 'interactions' ? 'btn btn-primary' : 'btn';
|
||||
}
|
||||
|
||||
// 加载数据库数据
|
||||
async function loadDatabaseData() {
|
||||
try {
|
||||
// 加载统计数据
|
||||
const statsRes = await fetch(`${API_BASE}/api/statistics`);
|
||||
const statsData = await statsRes.json();
|
||||
|
||||
if (statsData.success) {
|
||||
const stats = statsData.data;
|
||||
document.getElementById('dbTotalSites').textContent = stats.total_sites || 0;
|
||||
document.getElementById('dbTotalClicks').textContent = stats.total_clicks || 0;
|
||||
document.getElementById('dbTotalReplies').textContent = stats.total_replies || 0;
|
||||
document.getElementById('dbSuccessful').textContent = stats.successful_interactions || 0;
|
||||
}
|
||||
|
||||
// 加载站点数据
|
||||
await loadDatabaseSites();
|
||||
await loadDatabaseClicks();
|
||||
await loadDatabaseInteractions();
|
||||
} catch (error) {
|
||||
showToast('加载数据库数据失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载站点列表
|
||||
async function loadDatabaseSites() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/urls`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('dbSitesTableBody');
|
||||
if (data.success && data.data && data.data.length > 0) {
|
||||
tbody.innerHTML = data.data.map(site => `
|
||||
<tr>
|
||||
<td>${site.id}</td>
|
||||
<td>${site.site_name || '-'}</td>
|
||||
<td><a href="${site.site_url}" target="_blank" class="table-url" title="${site.site_url}">${site.site_url}</a></td>
|
||||
<td><span class="tag ${site.status === 'active' ? 'tag-success' : 'tag-error'}">${site.status === 'active' ? '活跃' : '未激活'}</span></td>
|
||||
<td>${site.click_count || 0}</td>
|
||||
<td>${site.reply_count || 0}</td>
|
||||
<td>${site.created_at || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">暂无站点数据</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载站点数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载点击记录
|
||||
async function loadDatabaseClicks() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/clicks?limit=100`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('dbClicksTableBody');
|
||||
if (data.success && data.data && data.data.length > 0) {
|
||||
tbody.innerHTML = data.data.map(click => `
|
||||
<tr>
|
||||
<td>${click.id}</td>
|
||||
<td>${click.site_id || '-'}</td>
|
||||
<td><span class="table-url" title="${click.site_url || ''}">${(click.site_url || '-').substring(0, 50)}...</span></td>
|
||||
<td>${click.click_time || '-'}</td>
|
||||
<td>${click.device_type || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">暂无点击记录</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载点击记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载互动记录
|
||||
async function loadDatabaseInteractions() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/interactions?limit=100`);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('dbInteractionsTableBody');
|
||||
if (data.success && data.data && data.data.length > 0) {
|
||||
tbody.innerHTML = data.data.map(interaction => `
|
||||
<tr>
|
||||
<td>${interaction.id}</td>
|
||||
<td>${interaction.site_id || '-'}</td>
|
||||
<td>${interaction.interaction_time || '-'}</td>
|
||||
<td><span class="table-url" title="${interaction.reply_content || ''}">${(interaction.reply_content || '-').substring(0, 30)}...</span></td>
|
||||
<td><span class="tag ${interaction.response_received ? 'tag-success' : 'tag-error'}">${interaction.response_received ? '是' : '否'}</span></td>
|
||||
<td><span class="tag ${interaction.interaction_status === 'success' ? 'tag-success' : 'tag-error'}">${interaction.interaction_status || '-'}</span></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">暂无互动记录</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载互动记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据库数据
|
||||
function refreshDatabaseData() {
|
||||
showToast('正在刷新数据...', 'info');
|
||||
loadDatabaseData();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
function init() {
|
||||
getSchedulerStatus();
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
<span class="menu-icon">🔗</span>
|
||||
<span>链接管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="location.href='database.html'">
|
||||
<span class="menu-icon">💾</span>
|
||||
<span>数据库管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="location.href='browser.html'">
|
||||
<span class="menu-icon">🌐</span>
|
||||
<span>浏览器测试</span>
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
<span class="menu-icon">🔗</span>
|
||||
<span>链接管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="location.href='database.html'">
|
||||
<span class="menu-icon">💾</span>
|
||||
<span>数据库管理</span>
|
||||
</div>
|
||||
<div class="menu-item" onclick="location.href='browser.html'">
|
||||
<span class="menu-icon">🌐</span>
|
||||
<span>浏览器测试</span>
|
||||
|
||||
65
stop_production.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MIP广告自动点击系统 - 停止服务脚本
|
||||
|
||||
set -e
|
||||
|
||||
echo "============================================================"
|
||||
echo "停止MIP广告自动点击系统"
|
||||
echo "============================================================"
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# 检查PID文件
|
||||
if [ -f "app.pid" ]; then
|
||||
PID=$(cat app.pid)
|
||||
echo "找到PID文件: $PID"
|
||||
|
||||
# 检查进程是否存在
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}正在停止服务 (PID: $PID)...${NC}"
|
||||
kill $PID
|
||||
|
||||
# 等待进程结束
|
||||
sleep 2
|
||||
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}进程未响应,强制终止...${NC}"
|
||||
kill -9 $PID
|
||||
fi
|
||||
|
||||
rm -f app.pid
|
||||
echo -e "${GREEN}✓ 服务已停止${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}进程不存在,清理PID文件${NC}"
|
||||
rm -f app.pid
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}未找到PID文件,尝试通过端口查找进程...${NC}"
|
||||
|
||||
# 通过端口查找进程
|
||||
PORT=5000
|
||||
PID=$(lsof -ti :$PORT 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$PID" ]; then
|
||||
echo "找到占用端口 $PORT 的进程: $PID"
|
||||
echo -e "${YELLOW}正在停止...${NC}"
|
||||
kill $PID
|
||||
sleep 2
|
||||
echo -e "${GREEN}✓ 服务已停止${NC}"
|
||||
else
|
||||
echo -e "${GREEN}没有运行中的服务${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "服务已停止"
|
||||
echo "============================================================"
|
||||
3
test/20260115_111005_health_baidu_com/info.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
测试时间: 2026-01-15 11:10:05
|
||||
测试URL: https://health.baidu.com/m/detail/ar_2366617956693492811
|
||||
测试环境: development
|
||||
3
test/20260115_111338_health_baidu_com/info.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
测试时间: 2026-01-15 11:13:38
|
||||
测试URL: https://health.baidu.com/m/detail/ar_2366617956693492811
|
||||
测试环境: development
|
||||
BIN
test/20260115_111346_health_baidu_com/01_before_click.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
test/20260115_111346_health_baidu_com/02_after_click.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
test/20260115_111346_health_baidu_com/debug_no_input.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
3
test/20260115_111346_health_baidu_com/info.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
测试时间: 2026-01-15 11:13:46
|
||||
测试URL: https://health.baidu.com/m/detail/ar_2366617956693492811
|
||||
测试环境: development
|
||||
BIN
test/20260115_111704_health_baidu_com/01_before_click.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
test/20260115_111704_health_baidu_com/02_after_click.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
test/20260115_111704_health_baidu_com/debug_no_input.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
3
test/20260115_111704_health_baidu_com/info.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
测试时间: 2026-01-15 11:17:04
|
||||
测试URL: https://health.baidu.com/m/detail/ar_2366617956693492811
|
||||
测试环境: development
|
||||
BIN
test/20260115_112020_health_baidu_com/01_before_click.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
3
test/20260115_112020_health_baidu_com/info.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
测试时间: 2026-01-15 11:20:20
|
||||
测试URL: https://health.baidu.com/m/detail/ar_2366617956693492811
|
||||
测试环境: development
|
||||
BIN
test/20260115_112239_health_baidu_com/01_before_click.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
test/20260115_112239_health_baidu_com/02_after_click.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
test/20260115_112239_health_baidu_com/debug_no_input.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
3
test/20260115_112239_health_baidu_com/info.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
测试时间: 2026-01-15 11:22:39
|
||||
测试URL: https://health.baidu.com/m/detail/ar_2366617956693492811
|
||||
测试环境: development
|
||||
1214
test/20260115_112239_health_baidu_com/page_html.txt
Normal file
BIN
test/20260115_113054_health_baidu_com/01_before_click.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
test/20260115_113054_health_baidu_com/02_after_click.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
test/20260115_113054_health_baidu_com/03_after_send.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
3
test/20260115_113054_health_baidu_com/info.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
测试时间: 2026-01-15 11:30:54
|
||||
测试URL: https://health.baidu.com/m/detail/ar_2366617956693492811
|
||||
测试环境: development
|
||||
1889
test/20260115_113054_health_baidu_com/page_html.txt
Normal file
BIN
test/20260115_113319_health_baidu_com/01_before_click.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
test/20260115_113319_health_baidu_com/02_after_click.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
test/20260115_113319_health_baidu_com/03_after_send.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
3
test/20260115_113319_health_baidu_com/info.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
测试时间: 2026-01-15 11:33:19
|
||||
测试URL: https://health.baidu.com/m/detail/ar_2366617956693492811
|
||||
测试环境: development
|
||||
1261
test/20260115_113319_health_baidu_com/page_html.txt
Normal file
@@ -6,17 +6,49 @@ AdsPower + Playwright CDP 集成测试
|
||||
from loguru import logger
|
||||
from adspower_client import AdsPowerClient
|
||||
from config import Config
|
||||
from db_manager import SiteManager, ClickManager, InteractionManager
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# 配置日志
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <level>{message}</level>",
|
||||
level="INFO"
|
||||
level="DEBUG" # 改为DEBUG级别
|
||||
)
|
||||
|
||||
|
||||
def create_test_folder(test_url: str):
|
||||
"""为每次测试创建独立的文件夹"""
|
||||
# 创建 test 目录
|
||||
test_base_dir = Path("./test")
|
||||
test_base_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 生成文件夹名:时间_域名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# 提取域名作为文件夹名称的一部分
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(test_url)
|
||||
domain = parsed.netloc.replace('.', '_').replace(':', '_')
|
||||
|
||||
# 创建测试文件夹
|
||||
test_folder = test_base_dir / f"{timestamp}_{domain}"
|
||||
test_folder.mkdir(exist_ok=True)
|
||||
|
||||
# 在文件夹中创建 info.txt 记录测试信息
|
||||
info_file = test_folder / "info.txt"
|
||||
with open(info_file, 'w', encoding='utf-8') as f:
|
||||
f.write(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write(f"测试URL: {test_url}\n")
|
||||
f.write(f"测试环境: {Config.ENV}\n")
|
||||
|
||||
return test_folder
|
||||
|
||||
|
||||
def test_adspower_connection(use_proxy: bool = False, proxy_info: dict = None, use_api_v1: bool = False):
|
||||
"""测试 AdsPower + Playwright CDP 连接
|
||||
|
||||
@@ -36,21 +68,71 @@ def test_adspower_connection(use_proxy: bool = False, proxy_info: dict = None, u
|
||||
# =====================================================
|
||||
|
||||
client = AdsPowerClient()
|
||||
site_id = None
|
||||
click_id = None
|
||||
sent_message = None
|
||||
|
||||
# ============ 新增:创建测试文件夹 ============
|
||||
test_folder = create_test_folder(TEST_URL)
|
||||
logger.info(f"测试文件夹: {test_folder}")
|
||||
|
||||
# 配置日志输出到文件
|
||||
log_file = test_folder / "test.log"
|
||||
logger.add(
|
||||
str(log_file),
|
||||
format="{time:HH:mm:ss} | {level: <8} | {message}",
|
||||
level="DEBUG"
|
||||
)
|
||||
logger.info("=" * 60)
|
||||
logger.info("开始测试")
|
||||
logger.info("=" * 60)
|
||||
# ============================================
|
||||
|
||||
try:
|
||||
# 0. 先查询 Profile 列表
|
||||
# ============ 新增:初始化数据库站点 ============
|
||||
logger.info("=" * 60)
|
||||
logger.info("步骤 0: 查询 Profile 列表")
|
||||
logger.info("初始化: 创建或获取测试站点")
|
||||
logger.info("=" * 60)
|
||||
|
||||
result = client.list_profiles()
|
||||
site_mgr = SiteManager()
|
||||
site = site_mgr.get_site_by_url(TEST_URL)
|
||||
|
||||
if not site:
|
||||
site_id = site_mgr.add_site(
|
||||
site_url=TEST_URL,
|
||||
site_name="测试站点-Playwright",
|
||||
site_dimension="医疗健康"
|
||||
)
|
||||
logger.info(f"✅ 创建测试站点: site_id={site_id}")
|
||||
else:
|
||||
site_id = site['id']
|
||||
logger.info(f"✅ 使用已存在站点: site_id={site_id}")
|
||||
|
||||
logger.info("")
|
||||
# ============================================
|
||||
# 0. 先根据环境查询分组,然后查询 Profile 列表
|
||||
logger.info("=" * 60)
|
||||
logger.info("步骤 0: 根据环境查询 Profile 列表")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 获取当前环境对应的分组ID
|
||||
group_id = client.get_group_by_env()
|
||||
if group_id:
|
||||
logger.info(f"当前环境: {Config.ENV}, 分组ID: {group_id}")
|
||||
else:
|
||||
logger.warning(f"未找到环境 {Config.ENV} 对应的分组,将查询所有Profile")
|
||||
|
||||
# 查询Profile列表(自动使用环境对应的分组)
|
||||
result = client.list_profiles(group_id=group_id)
|
||||
if not result:
|
||||
logger.error("查询 Profile 失败")
|
||||
return False
|
||||
|
||||
profiles = result.get('data', {}).get('list', [])
|
||||
if not profiles:
|
||||
logger.error("没有可用的 Profile,请先在 AdsPower 中创建 Profile")
|
||||
logger.error(f"在分组 {group_id} 中没有可用的 Profile")
|
||||
logger.error("请在 AdsPower 中创建 Profile 并分配到对应分组")
|
||||
logger.error(f"提示: {Config.ENV} 环境需要创建名为 '{'dev' if Config.ENV == 'development' else 'prod'}' 的分组")
|
||||
return False
|
||||
|
||||
# 使用第一个 Profile
|
||||
@@ -190,25 +272,16 @@ def test_adspower_connection(use_proxy: bool = False, proxy_info: dict = None, u
|
||||
|
||||
# 访问配置的网页
|
||||
logger.info(f"访问测试页面: {TEST_URL}")
|
||||
page.goto(TEST_URL, wait_until='domcontentloaded')
|
||||
page.goto(TEST_URL, wait_until='domcontentloaded', timeout=60000)
|
||||
|
||||
# 等待广告接口响应完成
|
||||
logger.info("等待广告接口响应...")
|
||||
# 等待页面完全加载
|
||||
logger.info("等待页面完全加载...")
|
||||
import time
|
||||
time.sleep(3)
|
||||
try:
|
||||
# 等待广告接口请求完成
|
||||
response = page.wait_for_response(
|
||||
lambda r: 'getRefreshAdAssets' in r.url and r.status == 200,
|
||||
timeout=10000 # 10秒超时
|
||||
)
|
||||
logger.info(f"广告接口已响应: {response.url}")
|
||||
|
||||
# 等待DOM更新
|
||||
import time
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
logger.warning(f"等待广告接口超时或失败: {str(e)}")
|
||||
logger.info("继续执行...")
|
||||
import time
|
||||
page.wait_for_load_state('networkidle', timeout=10000)
|
||||
except Exception:
|
||||
logger.warning("网络空闲超时,继续执行")
|
||||
time.sleep(2)
|
||||
|
||||
# 获取页面标题
|
||||
@@ -220,8 +293,8 @@ def test_adspower_connection(use_proxy: bool = False, proxy_info: dict = None, u
|
||||
logger.info(f"当前 URL: {current_url}")
|
||||
|
||||
# 截图测试(点击前)
|
||||
screenshot_path = "./test_screenshot_before.png"
|
||||
page.screenshot(path=screenshot_path)
|
||||
screenshot_path = test_folder / "01_before_click.png"
|
||||
page.screenshot(path=str(screenshot_path))
|
||||
logger.info(f"截图已保存: {screenshot_path}")
|
||||
|
||||
# 查找并点击广告
|
||||
@@ -246,46 +319,338 @@ def test_adspower_connection(use_proxy: bool = False, proxy_info: dict = None, u
|
||||
first_ad.scroll_into_view_if_needed()
|
||||
time.sleep(1)
|
||||
|
||||
# 点击广告
|
||||
# 记录点击前的URL
|
||||
old_url = page.url
|
||||
logger.info(f"点击前URL: {old_url}")
|
||||
|
||||
# 点击广告(页面内跳转)
|
||||
first_ad.click()
|
||||
logger.info("✅ 已点击第一个广告")
|
||||
|
||||
# ============ 记录点击到数据库 ============
|
||||
click_mgr = ClickManager()
|
||||
click_id = click_mgr.record_click(
|
||||
site_id=site_id,
|
||||
site_url=TEST_URL,
|
||||
user_ip=None,
|
||||
device_type='pc'
|
||||
)
|
||||
logger.info(f"✅ 已记录点击: click_id={click_id}")
|
||||
# ============================================
|
||||
|
||||
# 等待页面跳转
|
||||
time.sleep(3)
|
||||
page.wait_for_load_state('domcontentloaded')
|
||||
|
||||
# 获取点击后的页面信息
|
||||
# 获取跳转后的URL
|
||||
new_url = page.url
|
||||
new_title = page.title()
|
||||
logger.info(f"点击后 URL: {new_url}")
|
||||
logger.info(f"点击后标题: {new_title}")
|
||||
logger.info(f"跳转后URL: {new_url}")
|
||||
logger.info(f"跳转后标题: {new_title}")
|
||||
|
||||
# 截图(跳转后)
|
||||
screenshot_path_after = test_folder / "02_after_click.png"
|
||||
page.screenshot(path=str(screenshot_path_after))
|
||||
logger.info(f"跳转后截图已保存: {screenshot_path_after}")
|
||||
|
||||
# ============ 新增:发送咨询消息 ============
|
||||
logger.info("")
|
||||
logger.info("-" * 60)
|
||||
logger.info("开始发送咨询消息...")
|
||||
|
||||
# 预设消息列表
|
||||
consultation_messages = [
|
||||
"我想要预约一个医生,有什么推荐吗?",
|
||||
"我现在本人不在当地,医生什么时候有空,是随时能去吗?有没有推荐的医生。",
|
||||
"咱们医院是周六日是否上班,随时去吗?",
|
||||
"想找医生看看,有没有推荐的医生",
|
||||
"最近很不舒服,也说不出来全部的症状,能不能直接对话医生?"
|
||||
]
|
||||
|
||||
# 随机选择一条消息
|
||||
import random
|
||||
message = random.choice(consultation_messages)
|
||||
logger.info(f"选择的消息: {message}")
|
||||
|
||||
# 等待输入框加载
|
||||
time.sleep(2)
|
||||
|
||||
# 滚动到页面底部,确保输入框可见
|
||||
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
time.sleep(1)
|
||||
|
||||
# 输出页面HTML用于调试
|
||||
logger.info("正在分析页面结构...")
|
||||
html_content = page.content()
|
||||
html_file = test_folder / "page_html.txt"
|
||||
with open(html_file, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
logger.info(f"页面HTML已保存: {html_file}")
|
||||
|
||||
# 尝试查找输入框(通用策略)
|
||||
input_selectors = [
|
||||
# contenteditable 类型
|
||||
"textarea[contenteditable='true']",
|
||||
"div[contenteditable='true']",
|
||||
"*[contenteditable='true']",
|
||||
# 直接查找textarea
|
||||
"textarea",
|
||||
# 常见的 textarea
|
||||
"textarea[placeholder]",
|
||||
"textarea.input",
|
||||
"textarea[class*='input']",
|
||||
"textarea[class*='text']",
|
||||
"textarea[class*='box']",
|
||||
"textarea[class*='chat']",
|
||||
"textarea[class*='message']",
|
||||
# 常见的 input
|
||||
"input[type='text'][placeholder]",
|
||||
"input[class*='input']",
|
||||
"input[class*='text']",
|
||||
"input[class*='chat']",
|
||||
"input[class*='message']",
|
||||
# 全局备选
|
||||
"input[type='text']"
|
||||
]
|
||||
|
||||
input_found = False
|
||||
for selector in input_selectors:
|
||||
try:
|
||||
logger.debug(f"尝试选择器: {selector}")
|
||||
count = page.locator(selector).count()
|
||||
logger.debug(f" 找到 {count} 个匹配元素")
|
||||
|
||||
if count > 0:
|
||||
# 遍历所有匹配的元素,找第一个可见的
|
||||
for i in range(count):
|
||||
try:
|
||||
input_elem = page.locator(selector).nth(i)
|
||||
is_visible = input_elem.is_visible(timeout=1000)
|
||||
logger.debug(f" 元素 {i}: 可见={is_visible}")
|
||||
|
||||
if is_visible:
|
||||
logger.info(f"✅ 找到可见输入框: {selector} (第{i}个)")
|
||||
|
||||
# 滚动到输入框可见区域
|
||||
input_elem.scroll_into_view_if_needed()
|
||||
time.sleep(0.5)
|
||||
|
||||
# 点击输入框获取焦点
|
||||
input_elem.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# 输入消息
|
||||
input_elem.fill(message)
|
||||
logger.info("✅ 已输入消息")
|
||||
time.sleep(1)
|
||||
|
||||
# 保存已发送的消息
|
||||
sent_message = message
|
||||
|
||||
input_found = True
|
||||
break
|
||||
except Exception as e2:
|
||||
logger.debug(f" 元素 {i} 失败: {str(e2)}")
|
||||
continue
|
||||
|
||||
if input_found:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f" 失败: {str(e)}")
|
||||
continue
|
||||
|
||||
if not input_found:
|
||||
logger.warning("⚠️ 未找到输入框")
|
||||
debug_screenshot = test_folder / "debug_no_input.png"
|
||||
page.screenshot(path=str(debug_screenshot))
|
||||
logger.info(f"已保存调试截图: {debug_screenshot}")
|
||||
|
||||
# 兔底方案:点击页面底部上方位置,然后输入
|
||||
logger.info("尝试兔底方案:点击页面底部区域...")
|
||||
try:
|
||||
# 获取页面高度
|
||||
viewport_height = page.viewport_size['height']
|
||||
# 点击底部上方10px的位置(水平居中)
|
||||
click_x = page.viewport_size['width'] // 2
|
||||
click_y = viewport_height - 10
|
||||
|
||||
logger.debug(f"点击位置: ({click_x}, {click_y})")
|
||||
page.mouse.click(click_x, click_y)
|
||||
time.sleep(1)
|
||||
|
||||
# 直接输入文本
|
||||
page.keyboard.type(message, delay=50)
|
||||
logger.info("✅ 已输入消息(兔底方案)")
|
||||
time.sleep(1)
|
||||
|
||||
sent_message = message
|
||||
input_found = True
|
||||
except Exception as e:
|
||||
logger.error(f"兔底方案失败: {str(e)}")
|
||||
else:
|
||||
# 尝试发送消息
|
||||
message_sent = False
|
||||
|
||||
# 方法1: 先尝试按 Enter 键
|
||||
logger.info("尝试按 Enter 键发送...")
|
||||
try:
|
||||
page.keyboard.press('Enter')
|
||||
logger.info("✅ 已按 Enter 键")
|
||||
time.sleep(2)
|
||||
message_sent = True
|
||||
except Exception as e:
|
||||
logger.warning(f"按 Enter 键失败: {str(e)}")
|
||||
|
||||
# 方法2: 如果 Enter 键失败,查找并点击发送按钮
|
||||
if not message_sent:
|
||||
send_button_selectors = [
|
||||
# 按文本查找
|
||||
"button:has-text('发送')",
|
||||
"a:has-text('发送')",
|
||||
"span:has-text('发送')",
|
||||
"div:has-text('发送')",
|
||||
# 按类名查找
|
||||
"button[class*='send']",
|
||||
"button[class*='submit']",
|
||||
"a[class*='send']",
|
||||
"div[class*='send']",
|
||||
"span[class*='send']",
|
||||
# 按类型查找
|
||||
"button[type='submit']",
|
||||
# 全局备选
|
||||
"button"
|
||||
]
|
||||
|
||||
for selector in send_button_selectors:
|
||||
try:
|
||||
send_btn = page.locator(selector).first
|
||||
if send_btn.is_visible() and send_btn.is_enabled():
|
||||
send_btn.click()
|
||||
logger.info(f"✅ 已点击发送按钮: {selector}")
|
||||
message_sent = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if message_sent:
|
||||
logger.info("✅✅✅ 消息发送成功!")
|
||||
time.sleep(2)
|
||||
|
||||
# ============ 记录互动到数据库 ============
|
||||
interaction_mgr = InteractionManager()
|
||||
interaction_id = interaction_mgr.record_interaction(
|
||||
site_id=site_id,
|
||||
click_id=click_id,
|
||||
interaction_type='message', # 修复:使用 'message' 而非 'consultation'
|
||||
reply_content=sent_message,
|
||||
is_successful=True,
|
||||
response_received=False, # 后续可以添加检测逻辑
|
||||
response_content=None
|
||||
)
|
||||
logger.info(f"✅ 已记录互动: interaction_id={interaction_id}")
|
||||
# ============================================
|
||||
|
||||
# 截图(发送后)
|
||||
screenshot_path_sent = test_folder / "03_after_send.png"
|
||||
page.screenshot(path=str(screenshot_path_sent))
|
||||
logger.info(f"发送后截图已保存: {screenshot_path_sent}")
|
||||
else:
|
||||
logger.warning("⚠️ 未能发送消息")
|
||||
|
||||
# ============================================
|
||||
|
||||
# 截图(点击后)
|
||||
screenshot_path_after = "./test_screenshot_after.png"
|
||||
page.screenshot(path=screenshot_path_after)
|
||||
logger.info(f"点击后截图已保存: {screenshot_path_after}")
|
||||
else:
|
||||
logger.warning("⚠ 未找到广告元素")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查找/点击广告失败: {str(e)}")
|
||||
logger.error(f"查找/点击广告或发送消息失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# 保存错误时的截图
|
||||
page.screenshot(path="./test_screenshot_error.png")
|
||||
logger.info("错误截图已保存: ./test_screenshot_error.png")
|
||||
try:
|
||||
error_screenshot = test_folder / "error.png"
|
||||
page.screenshot(path=str(error_screenshot))
|
||||
logger.info(f"错误截图已保存: {error_screenshot}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 5. 清理资源
|
||||
logger.info("")
|
||||
logger.info("=" * 60)
|
||||
logger.info("步骤 5: 测试完成")
|
||||
logger.info("步骤 5: 测试完成 - 查询数据库记录")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 注意:不停止浏览器,保持运行状态供手动操作
|
||||
logger.info("浏览器保持运行状态,可继续手动操作")
|
||||
logger.info("如需停止浏览器,请在AdsPower中手动关闭")
|
||||
# ============ 查询数据库记录 ============
|
||||
if site_id:
|
||||
logger.info(f"\n站点ID: {site_id}")
|
||||
logger.info("-" * 60)
|
||||
|
||||
# 查询点击记录
|
||||
click_mgr = ClickManager()
|
||||
clicks = click_mgr.get_clicks_by_site(site_id, limit=5)
|
||||
click_count = click_mgr.get_click_count_by_site(site_id)
|
||||
logger.info(f"总点击次数: {click_count}")
|
||||
if clicks:
|
||||
last_click = clicks[0]
|
||||
logger.info(f"最新点击时间: {last_click['click_time']}")
|
||||
|
||||
# 查询互动记录
|
||||
interaction_mgr = InteractionManager()
|
||||
interactions = interaction_mgr.get_interactions_by_site(site_id, limit=5)
|
||||
success_count = interaction_mgr.get_successful_interactions_count(site_id)
|
||||
logger.info(f"成功互动次数: {success_count}")
|
||||
if interactions:
|
||||
last_interaction = interactions[0]
|
||||
logger.info(f"最新互动内容: {last_interaction['reply_content']}")
|
||||
logger.info(f"是否收到回复: {'是' if last_interaction['response_received'] else '否'}")
|
||||
# ============================================
|
||||
|
||||
logger.info("")
|
||||
logger.info("=" * 60)
|
||||
logger.info("测试完成!浏览器未关闭")
|
||||
logger.info("=" * 60)
|
||||
# 关闭浏览器前,截图聊天页面最终状态
|
||||
try:
|
||||
logger.info("截图聊天页面...")
|
||||
# 等待可能的回复消息加载
|
||||
time.sleep(2)
|
||||
# 滚动到页面顶部,确保看到完整对话
|
||||
page.evaluate("window.scrollTo(0, 0)")
|
||||
time.sleep(0.5)
|
||||
# 截图整个页面
|
||||
screenshot_path_final = test_folder / "04_final_chat.png"
|
||||
page.screenshot(path=str(screenshot_path_final), full_page=True)
|
||||
logger.info(f"✅ 聊天页面截图已保存: {screenshot_path_final}")
|
||||
except Exception as screenshot_err:
|
||||
logger.warning(f"截图失败: {str(screenshot_err)}")
|
||||
|
||||
logger.info("")
|
||||
# 优雅关闭 Playwright 连接,避免 CancelledError
|
||||
try:
|
||||
if browser:
|
||||
logger.debug("关闭 Playwright 浏览器连接...")
|
||||
browser.close()
|
||||
time.sleep(0.5)
|
||||
except Exception as close_err:
|
||||
logger.debug(f"关闭浏览器连接异常: {str(close_err)}")
|
||||
|
||||
# 根据配置决定是否关闭浏览器
|
||||
if Config.AUTO_CLOSE_BROWSER:
|
||||
logger.info("正在关闭浏览器...")
|
||||
try:
|
||||
client.stop_browser(user_id=profile_id)
|
||||
logger.info("✅ 浏览器已关闭")
|
||||
except Exception as e:
|
||||
logger.warning(f"关闭浏览器失败: {str(e)}")
|
||||
else:
|
||||
logger.info("浏览器保持运行状态,可继续手动操作")
|
||||
logger.info("如需停止浏览器,请在AdsPower中手动关闭")
|
||||
|
||||
logger.info("")
|
||||
logger.info("="*60)
|
||||
logger.info("测试完成!")
|
||||
if Config.AUTO_CLOSE_BROWSER:
|
||||
logger.info("浏览器已关闭")
|
||||
else:
|
||||
logger.info("浏览器未关闭")
|
||||
logger.info("="*60)
|
||||
|
||||
return True
|
||||
|
||||
@@ -309,8 +674,9 @@ def test_multiple_pages():
|
||||
logger.info("测试多页面操作")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 查询 Profile
|
||||
result = client.list_profiles()
|
||||
# 根据环境查询 Profile
|
||||
group_id = client.get_group_by_env()
|
||||
result = client.list_profiles(group_id=group_id)
|
||||
if not result:
|
||||
return False
|
||||
|
||||
|
||||
581
test_concurrent.py
Normal file
@@ -0,0 +1,581 @@
|
||||
"""
|
||||
并发测试:批量创建浏览器环境并执行广告点击+聊天操作
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from adspower_client import AdsPowerClient
|
||||
from config import Config
|
||||
from db_manager import SiteManager, ClickManager, InteractionManager
|
||||
import sys
|
||||
import time
|
||||
import random
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import List, Dict
|
||||
import threading # 添加线程锁支持
|
||||
|
||||
# 配置日志
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <level>{message}</level>",
|
||||
level="INFO"
|
||||
)
|
||||
|
||||
|
||||
class ConcurrentTester:
|
||||
"""并发测试管理器"""
|
||||
|
||||
# 类级别的浏览器启动锁,确保启动操作串行化
|
||||
_browser_start_lock = threading.Lock()
|
||||
|
||||
def __init__(self, test_url: str, max_workers: int = 3):
|
||||
"""
|
||||
初始化并发测试器
|
||||
|
||||
Args:
|
||||
test_url: 测试的目标URL
|
||||
max_workers: 最大并发数
|
||||
"""
|
||||
self.test_url = test_url
|
||||
self.max_workers = max_workers
|
||||
self.client = AdsPowerClient()
|
||||
self.created_profiles = [] # 记录创建的环境ID
|
||||
self.created_proxies = [] # 记录创建的代理ID
|
||||
|
||||
# 创建测试目录
|
||||
self.test_base_dir = Path("./test_concurrent")
|
||||
self.test_base_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 初始化数据库
|
||||
self.site_mgr = SiteManager()
|
||||
self.site_id = self._init_site()
|
||||
|
||||
def _init_site(self) -> int:
|
||||
"""初始化或获取站点"""
|
||||
site = self.site_mgr.get_site_by_url(self.test_url)
|
||||
if not site:
|
||||
site_id = self.site_mgr.add_site(
|
||||
site_url=self.test_url,
|
||||
site_name="并发测试站点",
|
||||
site_dimension="医疗健康"
|
||||
)
|
||||
logger.info(f"✅ 创建测试站点: site_id={site_id}")
|
||||
else:
|
||||
site_id = site['id']
|
||||
logger.info(f"✅ 使用已存在站点: site_id={site_id}")
|
||||
return site_id
|
||||
|
||||
def create_browser_profile(self, index: int) -> Dict:
|
||||
"""
|
||||
创建浏览器环境
|
||||
|
||||
Args:
|
||||
index: 环境编号
|
||||
|
||||
Returns:
|
||||
环境信息字典
|
||||
"""
|
||||
try:
|
||||
# 获取分组ID
|
||||
group_id = self.client.get_group_by_env()
|
||||
time.sleep(0.5) # API 调用间隔
|
||||
|
||||
# 获取大麦IP代理
|
||||
logger.info(f"[环境 {index}] 获取代理IP...")
|
||||
proxy_info = self.client.get_damai_proxy()
|
||||
time.sleep(0.5) # API 调用间隔
|
||||
|
||||
if not proxy_info:
|
||||
logger.warning(f"[环境 {index}] 获取代理失败,将使用随机代理")
|
||||
proxy_config = {}
|
||||
proxy_id = None
|
||||
else:
|
||||
logger.info(f"[环境 {index}] 代理IP: {proxy_info['host']}:{proxy_info['port']}")
|
||||
|
||||
# 创建代理并记录ID
|
||||
proxy_data = {
|
||||
"type": "http",
|
||||
"host": proxy_info["host"],
|
||||
"port": proxy_info["port"],
|
||||
"user": self.client.DAMAI_USER,
|
||||
"password": self.client.DAMAI_PASSWORD,
|
||||
"remark": f"并发测试代理_{index}"
|
||||
}
|
||||
|
||||
proxy_id = self.client.create_proxy(proxy_data)
|
||||
time.sleep(0.5) # API 调用间隔
|
||||
|
||||
if proxy_id:
|
||||
self.created_proxies.append(proxy_id)
|
||||
logger.info(f"[环境 {index}] 创建代理: {proxy_id}")
|
||||
proxy_config = {"proxyid": proxy_id}
|
||||
else:
|
||||
logger.warning(f"[环境 {index}] 创建代理失败")
|
||||
proxy_config = {}
|
||||
proxy_id = None
|
||||
|
||||
profile_data = {
|
||||
"name": f"并发测试_{index}_{datetime.now().strftime('%H%M%S')}",
|
||||
"group_id": str(group_id) if group_id else "0",
|
||||
"platform": "health.baidu.com",
|
||||
"tabs": [self.test_url],
|
||||
"repeat_config": [],
|
||||
"ignore_cookie_error": "1",
|
||||
"country": "cn",
|
||||
"city": "beijing",
|
||||
"remark": f"并发测试环境 #{index}",
|
||||
"fingerprint_config": {
|
||||
"automatic_timezone": "1",
|
||||
"flash": "block",
|
||||
"scan_port_type": "1",
|
||||
"location": "ask",
|
||||
"location_switch": "1",
|
||||
"canvas": "0",
|
||||
"webgl": "0",
|
||||
"audio": "0",
|
||||
"webrtc": "local",
|
||||
"do_not_track": "true",
|
||||
"hardware_concurrency": "default",
|
||||
"device_memory": "default",
|
||||
"gpu": "2",
|
||||
"mac_address_config": {
|
||||
"model": "1",
|
||||
"address": ""
|
||||
},
|
||||
"browser_kernel_config": {
|
||||
"version": "latest",
|
||||
"type": "chrome"
|
||||
},
|
||||
"random_ua": {
|
||||
"ua_system_version": ["Windows"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 如果有代理配置,添加到profile_data
|
||||
if proxy_config:
|
||||
profile_data.update(proxy_config)
|
||||
|
||||
response = self.client._make_request(
|
||||
'POST',
|
||||
'/api/v2/browser-profile/create',
|
||||
json=profile_data
|
||||
)
|
||||
|
||||
if response and response.get('code') == 0:
|
||||
profile_id = response.get('data', {}).get('profile_id')
|
||||
logger.info(f"✅ 创建环境 #{index}: {profile_id}")
|
||||
self.created_profiles.append(profile_id)
|
||||
return {
|
||||
'index': index,
|
||||
'profile_id': profile_id,
|
||||
'name': profile_data['name'],
|
||||
'proxy': proxy_info if proxy_info else None
|
||||
}
|
||||
else:
|
||||
logger.error(f"❌ 创建环境 #{index} 失败: {response}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 创建环境 #{index} 异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def run_single_task(self, profile_info: Dict) -> Dict:
|
||||
"""
|
||||
执行单个浏览器任务
|
||||
|
||||
Args:
|
||||
profile_info: 环境信息
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
index = profile_info['index']
|
||||
profile_id = profile_info['profile_id']
|
||||
|
||||
result = {
|
||||
'index': index,
|
||||
'profile_id': profile_id,
|
||||
'success': False,
|
||||
'click_id': None,
|
||||
'interaction_id': None,
|
||||
'error': None
|
||||
}
|
||||
|
||||
# 创建任务文件夹
|
||||
task_folder = self.test_base_dir / f"task_{index}_{datetime.now().strftime('%H%M%S')}"
|
||||
task_folder.mkdir(exist_ok=True)
|
||||
|
||||
# 每个线程创建自己的 AdsPowerClient 实例
|
||||
client = AdsPowerClient()
|
||||
|
||||
try:
|
||||
logger.info(f"[任务 {index}] 启动浏览器: {profile_id}")
|
||||
|
||||
# 使用类锁确保浏览器启动串行化,避免 API 频率限制
|
||||
with self._browser_start_lock:
|
||||
logger.debug(f"[任务 {index}] 获取启动锁...")
|
||||
# 启动浏览器
|
||||
browser_info = client.start_browser(user_id=profile_id)
|
||||
if not browser_info:
|
||||
result['error'] = "启动浏览器失败"
|
||||
return result
|
||||
# 启动后等待,避免下一个启动请求过快
|
||||
time.sleep(1.5)
|
||||
logger.debug(f"[任务 {index}] 释放启动锁")
|
||||
|
||||
time.sleep(1) # 额外等待浏览器完全启动
|
||||
|
||||
# 连接浏览器
|
||||
browser = client.connect_browser(browser_info)
|
||||
if not browser:
|
||||
result['error'] = "CDP连接失败"
|
||||
return result
|
||||
|
||||
# 获取页面
|
||||
context = browser.contexts[0]
|
||||
pages = context.pages
|
||||
|
||||
# 清理多余页面
|
||||
for p in pages:
|
||||
if 'start.adspower.net' in p.url:
|
||||
pages.remove(p)
|
||||
|
||||
if pages:
|
||||
page = pages[0]
|
||||
else:
|
||||
page = context.new_page()
|
||||
|
||||
logger.info(f"[任务 {index}] 访问页面: {self.test_url}")
|
||||
page.goto(self.test_url, wait_until='domcontentloaded', timeout=60000)
|
||||
time.sleep(3)
|
||||
|
||||
# 等待页面完全加载
|
||||
try:
|
||||
page.wait_for_load_state('networkidle', timeout=10000)
|
||||
except Exception:
|
||||
logger.warning(f"[任务 {index}] 网络空闲超时,继续执行")
|
||||
time.sleep(2)
|
||||
|
||||
# 截图
|
||||
page.screenshot(path=str(task_folder / "01_loaded.png"))
|
||||
|
||||
# 查找并点击广告
|
||||
ad_selector = 'span.ec-tuiguang.ecfc-tuiguang.xz81bbe'
|
||||
ad_elements = page.locator(ad_selector)
|
||||
ad_count = ad_elements.count()
|
||||
|
||||
logger.info(f"[任务 {index}] 找到 {ad_count} 个广告")
|
||||
|
||||
if ad_count > 0:
|
||||
# 点击第一个广告
|
||||
first_ad = ad_elements.first
|
||||
first_ad.scroll_into_view_if_needed()
|
||||
time.sleep(1)
|
||||
|
||||
# 点击,忽略超时错误
|
||||
try:
|
||||
first_ad.click(timeout=60000)
|
||||
logger.info(f"[任务 {index}] ✅ 已点击广告")
|
||||
except Exception as click_err:
|
||||
logger.warning(f"[任务 {index}] 点击超时,但可能已跳转")
|
||||
|
||||
# 记录点击
|
||||
click_mgr = ClickManager()
|
||||
click_id = click_mgr.record_click(
|
||||
site_id=self.site_id,
|
||||
site_url=self.test_url,
|
||||
user_ip=None,
|
||||
device_type='pc'
|
||||
)
|
||||
result['click_id'] = click_id
|
||||
|
||||
# 等待跳转
|
||||
time.sleep(3)
|
||||
page.wait_for_load_state('domcontentloaded')
|
||||
page.screenshot(path=str(task_folder / "02_after_click.png"))
|
||||
|
||||
# 发送消息
|
||||
messages = [
|
||||
"我想要预约一个医生,有什么推荐吗?",
|
||||
"我现在本人不在当地,医生什么时候有空,是随时能去吗?",
|
||||
"咱们医院是周六日是否上班,随时去吗?",
|
||||
"想找医生看看,有没有推荐的医生",
|
||||
"最近很不舒服,也说不出来全部的症状,能不能直接对话医生?"
|
||||
]
|
||||
message = random.choice(messages)
|
||||
|
||||
# 滚动到底部
|
||||
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
time.sleep(1)
|
||||
|
||||
# 查找输入框
|
||||
input_selectors = [
|
||||
"textarea[contenteditable='true']",
|
||||
"textarea",
|
||||
"textarea[placeholder]",
|
||||
"input[type='text']"
|
||||
]
|
||||
|
||||
input_found = False
|
||||
for selector in input_selectors:
|
||||
try:
|
||||
count = page.locator(selector).count()
|
||||
if count > 0:
|
||||
for i in range(count):
|
||||
input_elem = page.locator(selector).nth(i)
|
||||
if input_elem.is_visible(timeout=1000):
|
||||
input_elem.scroll_into_view_if_needed()
|
||||
time.sleep(0.5)
|
||||
input_elem.click()
|
||||
time.sleep(0.5)
|
||||
input_elem.fill(message)
|
||||
logger.info(f"[任务 {index}] ✅ 已输入消息")
|
||||
input_found = True
|
||||
break
|
||||
if input_found:
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
# 兜底方案
|
||||
if not input_found:
|
||||
logger.warning(f"[任务 {index}] 未找到输入框,尝试兜底方案...")
|
||||
try:
|
||||
# 检查 viewport_size 是否为 None
|
||||
if page.viewport_size is None:
|
||||
logger.warning(f"[任务 {index}] viewport_size 为 None,设置默认视口")
|
||||
# 设置默认视口大小
|
||||
page.set_viewport_size({"width": 1280, "height": 720})
|
||||
time.sleep(0.5)
|
||||
|
||||
viewport_height = page.viewport_size['height']
|
||||
click_x = page.viewport_size['width'] // 2
|
||||
click_y = viewport_height - 10
|
||||
|
||||
logger.debug(f"[任务 {index}] 点击位置: ({click_x}, {click_y})")
|
||||
page.mouse.click(click_x, click_y)
|
||||
time.sleep(1)
|
||||
page.keyboard.type(message, delay=50)
|
||||
logger.info(f"[任务 {index}] ✅ 已输入消息(兜底)")
|
||||
input_found = True
|
||||
except Exception as fallback_err:
|
||||
logger.error(f"[任务 {index}] 兜底方案失败: {str(fallback_err)}")
|
||||
|
||||
# 发送消息
|
||||
if input_found:
|
||||
try:
|
||||
page.keyboard.press('Enter')
|
||||
logger.info(f"[任务 {index}] ✅ 已发送消息")
|
||||
time.sleep(2)
|
||||
|
||||
# 记录互动
|
||||
interaction_mgr = InteractionManager()
|
||||
interaction_id = interaction_mgr.record_interaction(
|
||||
site_id=self.site_id,
|
||||
click_id=click_id,
|
||||
interaction_type='message',
|
||||
reply_content=message,
|
||||
is_successful=True,
|
||||
response_received=False,
|
||||
response_content=None
|
||||
)
|
||||
result['interaction_id'] = interaction_id
|
||||
|
||||
page.screenshot(path=str(task_folder / "03_sent.png"))
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
logger.warning(f"[任务 {index}] 发送失败: {str(e)}")
|
||||
|
||||
# 关闭浏览器前,截图聊天页面最终状态
|
||||
try:
|
||||
logger.info(f"[任务 {index}] 截图聊天页面...")
|
||||
# 等待可能的回复消息加载
|
||||
time.sleep(2)
|
||||
# 滚动到页面顶部,确保看到完整对话
|
||||
page.evaluate("window.scrollTo(0, 0)")
|
||||
time.sleep(0.5)
|
||||
# 截图整个页面
|
||||
page.screenshot(path=str(task_folder / "04_final_chat.png"), full_page=True)
|
||||
logger.info(f"[任务 {index}] ✅ 聊天页面截图已保存")
|
||||
except Exception as screenshot_err:
|
||||
logger.warning(f"[任务 {index}] 截图失败: {str(screenshot_err)}")
|
||||
|
||||
# 优雅关闭 Playwright 连接,避免 CancelledError
|
||||
try:
|
||||
if browser:
|
||||
logger.debug(f"[任务 {index}] 关闭 Playwright 浏览器连接...")
|
||||
browser.close()
|
||||
time.sleep(0.5)
|
||||
except Exception as close_err:
|
||||
logger.debug(f"[任务 {index}] 关闭浏览器连接异常: {str(close_err)}")
|
||||
|
||||
# 根据配置决定是否关闭浏览器进程
|
||||
if Config.AUTO_CLOSE_BROWSER:
|
||||
try:
|
||||
client.stop_browser(user_id=profile_id)
|
||||
logger.info(f"[任务 {index}] 浏览器已关闭")
|
||||
except Exception as stop_err:
|
||||
logger.warning(f"[任务 {index}] 关闭浏览器失败: {str(stop_err)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[任务 {index}] 执行异常: {str(e)}")
|
||||
result['error'] = str(e)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return result
|
||||
|
||||
def delete_profiles(self, profile_ids: List[str]):
|
||||
"""
|
||||
批量删除环境
|
||||
|
||||
Args:
|
||||
profile_ids: 环境ID列表
|
||||
"""
|
||||
if not profile_ids:
|
||||
return
|
||||
|
||||
try:
|
||||
response = self.client._make_request(
|
||||
'POST',
|
||||
'/api/v2/browser-profile/delete',
|
||||
json={'profile_id': profile_ids}
|
||||
)
|
||||
|
||||
if response and response.get('code') == 0:
|
||||
logger.info(f"✅ 已删除 {len(profile_ids)} 个环境")
|
||||
else:
|
||||
logger.error(f"❌ 删除环境失败: {response}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 删除环境异常: {str(e)}")
|
||||
|
||||
def delete_proxies(self, proxy_ids: List[str]):
|
||||
"""
|
||||
批量删除代理
|
||||
|
||||
Args:
|
||||
proxy_ids: 代理ID列表
|
||||
"""
|
||||
if not proxy_ids:
|
||||
return
|
||||
|
||||
try:
|
||||
response = self.client._make_request(
|
||||
'POST',
|
||||
'/api/v2/proxy-list/delete',
|
||||
json={'proxy_id': proxy_ids}
|
||||
)
|
||||
|
||||
if response and response.get('code') == 0:
|
||||
logger.info(f"✅ 已删除 {len(proxy_ids)} 个代理")
|
||||
else:
|
||||
logger.error(f"❌ 删除代理失败: {response}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 删除代理异帰常: {str(e)}")
|
||||
|
||||
def run_concurrent_test(self, num_tasks: int):
|
||||
"""
|
||||
运行并发测试
|
||||
|
||||
Args:
|
||||
num_tasks: 并发任务数量
|
||||
"""
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"开始并发测试: {num_tasks} 个任务, 最大并发数: {self.max_workers}")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 第一步:批量创建环境
|
||||
logger.info("\n步骤 1: 创建浏览器环境")
|
||||
logger.info("-" * 60)
|
||||
|
||||
profiles = []
|
||||
for i in range(num_tasks):
|
||||
profile_info = self.create_browser_profile(i + 1)
|
||||
if profile_info:
|
||||
profiles.append(profile_info)
|
||||
proxy_info = profile_info.get('proxy')
|
||||
if proxy_info:
|
||||
logger.info(f"环境 #{i+1} 使用代理: {proxy_info['host']}:{proxy_info['port']}")
|
||||
# 增加环境创建间隔,避免触发 API 频率限制
|
||||
time.sleep(3) # 每个环境创建后等待 3 秒
|
||||
|
||||
logger.info(f"✅ 成功创建 {len(profiles)} 个环境\n")
|
||||
|
||||
if not profiles:
|
||||
logger.error("没有成功创建任何环境,退出测试")
|
||||
return
|
||||
|
||||
# 第二步:并发执行任务
|
||||
logger.info("步骤 2: 并发执行任务")
|
||||
logger.info("-" * 60)
|
||||
|
||||
results = []
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
futures = {
|
||||
executor.submit(self.run_single_task, profile): profile
|
||||
for profile in profiles
|
||||
}
|
||||
|
||||
for future in as_completed(futures):
|
||||
profile = futures[future]
|
||||
try:
|
||||
result = future.result()
|
||||
results.append(result)
|
||||
|
||||
status = "✅ 成功" if result['success'] else "❌ 失败"
|
||||
logger.info(f"[任务 {result['index']}] {status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[任务 {profile['index']}] 执行异常: {str(e)}")
|
||||
|
||||
# 第三步:统计结果
|
||||
logger.info("\n步骤 3: 测试结果统计")
|
||||
logger.info("=" * 60)
|
||||
|
||||
success_count = sum(1 for r in results if r['success'])
|
||||
failed_count = len(results) - success_count
|
||||
|
||||
logger.info(f"总任务数: {len(results)}")
|
||||
logger.info(f"成功数: {success_count}")
|
||||
logger.info(f"失败数: {failed_count}")
|
||||
logger.info(f"成功率: {success_count/len(results)*100:.1f}%")
|
||||
|
||||
# 第四步:清理环境(可选)
|
||||
if Config.AUTO_CLOSE_BROWSER:
|
||||
logger.info("\n步骤 4: 清理测试环境")
|
||||
logger.info("-" * 60)
|
||||
|
||||
# 删除环境
|
||||
if self.created_profiles:
|
||||
self.delete_profiles(self.created_profiles)
|
||||
|
||||
# 删除代理
|
||||
if self.created_proxies:
|
||||
self.delete_proxies(self.created_proxies)
|
||||
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("并发测试完成!")
|
||||
logger.info("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("并发测试工具")
|
||||
logger.info(f"当前环境: {Config.ENV}")
|
||||
logger.info(f"AdsPower API: {Config.ADSPOWER_API_URL}")
|
||||
logger.info("")
|
||||
|
||||
# ==================== 配置区 ====================
|
||||
TEST_URL = "https://health.baidu.com/m/detail/ar_2366617956693492811"
|
||||
NUM_TASKS = 3 # 并发任务数
|
||||
MAX_WORKERS = 3 # 最大并发执行数(建议不超过3)
|
||||
# =====================================================
|
||||
|
||||
tester = ConcurrentTester(
|
||||
test_url=TEST_URL,
|
||||
max_workers=MAX_WORKERS
|
||||
)
|
||||
|
||||
tester.run_concurrent_test(num_tasks=NUM_TASKS)
|
||||
BIN
test_concurrent/task_1_124137/01_loaded.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
test_concurrent/task_1_162035/01_loaded.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
test_concurrent/task_1_162035/02_after_click.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
test_concurrent/task_1_162035/03_sent.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
test_concurrent/task_1_162334/01_loaded.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
test_concurrent/task_1_163149/01_loaded.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
test_concurrent/task_1_163149/02_after_click.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
test_concurrent/task_1_164914/01_loaded.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
test_concurrent/task_1_164914/02_after_click.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
test_concurrent/task_1_164914/03_sent.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
test_concurrent/task_1_164914/04_final_chat.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
test_concurrent/task_1_165521/01_loaded.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
test_concurrent/task_1_165521/04_final_chat.png
Normal file
|
After Width: | Height: | Size: 684 KiB |
BIN
test_concurrent/task_2_124137/01_loaded.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
test_concurrent/task_2_162035/01_loaded.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
test_concurrent/task_2_162035/02_after_click.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
test_concurrent/task_2_162035/03_sent.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
test_concurrent/task_2_162334/01_loaded.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
test_concurrent/task_2_162334/02_after_click.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
test_concurrent/task_2_162334/03_sent.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
test_concurrent/task_2_163149/01_loaded.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
test_concurrent/task_2_163149/02_after_click.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
test_concurrent/task_2_163635/01_loaded.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
test_concurrent/task_2_163635/02_after_click.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
test_concurrent/task_2_163635/03_sent.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
test_concurrent/task_2_164208/01_loaded.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
test_concurrent/task_2_164208/02_after_click.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
test_concurrent/task_2_164208/03_sent.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
test_concurrent/task_2_164914/01_loaded.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
test_concurrent/task_2_164914/04_final_chat.png
Normal file
|
After Width: | Height: | Size: 679 KiB |
BIN
test_concurrent/task_2_165521/01_loaded.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
test_concurrent/task_2_165521/02_after_click.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
test_concurrent/task_2_165521/03_sent.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
test_concurrent/task_2_165521/04_final_chat.png
Normal file
|
After Width: | Height: | Size: 73 KiB |