Upload 6 files
Browse files- api_server.py +915 -0
- index.html +1606 -0
- jetbrains.py +258 -0
- unibo_auth2_get_AuthCode_RefreshToken_sucsses.py +391 -0
- unibo_jetbrains_activation.py +725 -0
- 收发邮件.py +316 -0
api_server.py
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
import asyncio
|
| 7 |
+
from typing import Dict, List, Optional, Any
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
from fastapi import FastAPI, HTTPException, BackgroundTasks, Query, Depends, UploadFile, File, Body, Header
|
| 11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
+
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
+
import uvicorn
|
| 15 |
+
|
| 16 |
+
from unibo_jetbrains_activation import (
|
| 17 |
+
DatabaseManager, JetbrainsSubmitter, LinkExtractor, ProcessController,
|
| 18 |
+
STATUS_PENDING, STATUS_SUBMITTED, STATUS_FAILED, STATUS_LINK_EXTRACTED
|
| 19 |
+
)
|
| 20 |
+
from loguru import logger
|
| 21 |
+
|
| 22 |
+
# 配置日志
|
| 23 |
+
logger.remove()
|
| 24 |
+
logger.add("api_server.log", rotation="1 MB", level="INFO")
|
| 25 |
+
logger.add(lambda msg: print(msg, end=""), level="INFO")
|
| 26 |
+
|
| 27 |
+
# 创建FastAPI应用
|
| 28 |
+
app = FastAPI(
|
| 29 |
+
title="JetBrains激活链接管理系统",
|
| 30 |
+
description="管理JetBrains激活链接的获取流程,包括提交邮箱和提取链接",
|
| 31 |
+
version="1.0.0"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# 允许跨域请求
|
| 35 |
+
app.add_middleware(
|
| 36 |
+
CORSMiddleware,
|
| 37 |
+
allow_origins=["*"], # 在生产环境中应该替换为实际的前端域名
|
| 38 |
+
allow_credentials=True,
|
| 39 |
+
allow_methods=["*"],
|
| 40 |
+
allow_headers=["*"],
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# 创建全局的ProcessController实例
|
| 44 |
+
controller = ProcessController()
|
| 45 |
+
|
| 46 |
+
# 定义环境变量
|
| 47 |
+
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin123")
|
| 48 |
+
# API_TOKEN = os.environ.get("API_TOKEN", "unibo_token")
|
| 49 |
+
|
| 50 |
+
def verify_token(token: str = Header(None)):
|
| 51 |
+
if token != ADMIN_PASSWORD:
|
| 52 |
+
raise HTTPException(status_code=401, detail="无效的token")
|
| 53 |
+
|
| 54 |
+
# 定义请求和响应模型
|
| 55 |
+
class AccountItem(BaseModel):
|
| 56 |
+
id: Optional[int] = None
|
| 57 |
+
register_time: str
|
| 58 |
+
username: str
|
| 59 |
+
password: str
|
| 60 |
+
security_email: Optional[str] = None
|
| 61 |
+
status: int = 0
|
| 62 |
+
activation_link: Optional[str] = None
|
| 63 |
+
notes: Optional[str] = None
|
| 64 |
+
updated_at: Optional[str] = None
|
| 65 |
+
used: Optional[int] = 0
|
| 66 |
+
|
| 67 |
+
class AccountsResponse(BaseModel):
|
| 68 |
+
accounts: List[AccountItem]
|
| 69 |
+
total: int
|
| 70 |
+
|
| 71 |
+
class SubmitRequest(BaseModel):
|
| 72 |
+
account_ids: List[int] = []
|
| 73 |
+
max_workers: int = 3
|
| 74 |
+
proxy: Optional[str] = None
|
| 75 |
+
|
| 76 |
+
class ExtractRequest(BaseModel):
|
| 77 |
+
account_ids: List[int] = []
|
| 78 |
+
max_workers: int = 1
|
| 79 |
+
|
| 80 |
+
class ProcessResponse(BaseModel):
|
| 81 |
+
success_count: int
|
| 82 |
+
error_count: int
|
| 83 |
+
details: Optional[Dict[str, Any]] = None
|
| 84 |
+
|
| 85 |
+
class ImportRequest(BaseModel):
|
| 86 |
+
filepath: str
|
| 87 |
+
|
| 88 |
+
class ImportResponse(BaseModel):
|
| 89 |
+
imported_count: int
|
| 90 |
+
total_accounts: int
|
| 91 |
+
|
| 92 |
+
class ExportRequest(BaseModel):
|
| 93 |
+
status: Optional[int] = None
|
| 94 |
+
filename: Optional[str] = "export_accounts.txt"
|
| 95 |
+
|
| 96 |
+
class ExportResponse(BaseModel):
|
| 97 |
+
exported_count: int
|
| 98 |
+
filepath: str
|
| 99 |
+
|
| 100 |
+
class StatusCounts(BaseModel):
|
| 101 |
+
total: int = 0
|
| 102 |
+
pending: int = 0
|
| 103 |
+
submitted: int = 0
|
| 104 |
+
failed: int = 0
|
| 105 |
+
link_extracted: int = 0
|
| 106 |
+
unused: int = 0 # 新增
|
| 107 |
+
|
| 108 |
+
class StatusUpdateRequest(BaseModel):
|
| 109 |
+
status: int
|
| 110 |
+
notes: Optional[str] = None
|
| 111 |
+
activation_link: Optional[str] = None
|
| 112 |
+
|
| 113 |
+
class BatchToggleUsedRequest(BaseModel):
|
| 114 |
+
ids: list[int]
|
| 115 |
+
used: int
|
| 116 |
+
|
| 117 |
+
# 后台任务
|
| 118 |
+
running_tasks = {}
|
| 119 |
+
|
| 120 |
+
async def process_submit_task(task_id: str, account_ids: List[int], max_workers: int, proxy: Optional[str] = None):
|
| 121 |
+
"""后台提交邮箱任务(并发版)"""
|
| 122 |
+
try:
|
| 123 |
+
# 获取要处理的账号
|
| 124 |
+
accounts = []
|
| 125 |
+
with controller.db_manager.lock:
|
| 126 |
+
cursor = controller.db_manager.conn.cursor()
|
| 127 |
+
if account_ids:
|
| 128 |
+
# 处理指定的账号
|
| 129 |
+
for account_id in account_ids:
|
| 130 |
+
cursor.execute(
|
| 131 |
+
"SELECT id, register_time, username, password, security_email, status, activation_link FROM accounts WHERE id = ?",
|
| 132 |
+
(account_id,)
|
| 133 |
+
)
|
| 134 |
+
row = cursor.fetchone()
|
| 135 |
+
if row:
|
| 136 |
+
accounts.append({
|
| 137 |
+
'id': row[0],
|
| 138 |
+
'register_time': row[1],
|
| 139 |
+
'username': row[2],
|
| 140 |
+
'password': row[3],
|
| 141 |
+
'security_email': row[4],
|
| 142 |
+
'status': row[5],
|
| 143 |
+
'activation_link': row[6]
|
| 144 |
+
})
|
| 145 |
+
else:
|
| 146 |
+
# 处理所有未提交的账号
|
| 147 |
+
cursor.execute(
|
| 148 |
+
"SELECT id, register_time, username, password, security_email, status, activation_link FROM accounts WHERE status = ?",
|
| 149 |
+
(STATUS_PENDING,)
|
| 150 |
+
)
|
| 151 |
+
for row in cursor.fetchall():
|
| 152 |
+
accounts.append({
|
| 153 |
+
'id': row[0],
|
| 154 |
+
'register_time': row[1],
|
| 155 |
+
'username': row[2],
|
| 156 |
+
'password': row[3],
|
| 157 |
+
'security_email': row[4],
|
| 158 |
+
'status': row[5],
|
| 159 |
+
'activation_link': row[6]
|
| 160 |
+
})
|
| 161 |
+
|
| 162 |
+
# 设置代理(如果提供)
|
| 163 |
+
if proxy:
|
| 164 |
+
submitter = JetbrainsSubmitter(controller.db_manager, proxy)
|
| 165 |
+
else:
|
| 166 |
+
submitter = controller.submitter
|
| 167 |
+
|
| 168 |
+
success_count = 0
|
| 169 |
+
error_count = 0
|
| 170 |
+
results = []
|
| 171 |
+
|
| 172 |
+
sem = asyncio.Semaphore(max_workers)
|
| 173 |
+
|
| 174 |
+
async def handle_account(account):
|
| 175 |
+
nonlocal success_count, error_count
|
| 176 |
+
try:
|
| 177 |
+
async with sem:
|
| 178 |
+
controller.db_manager.log_operation(
|
| 179 |
+
account['username'],
|
| 180 |
+
"submit_email_background",
|
| 181 |
+
"processing",
|
| 182 |
+
f"任务ID: {task_id}"
|
| 183 |
+
)
|
| 184 |
+
# 提交邮箱(同步方法用run_in_executor)
|
| 185 |
+
loop = asyncio.get_running_loop()
|
| 186 |
+
result = await loop.run_in_executor(None, submitter.submit_email, account)
|
| 187 |
+
if result:
|
| 188 |
+
success_count += 1
|
| 189 |
+
else:
|
| 190 |
+
error_count += 1
|
| 191 |
+
await asyncio.sleep(1)
|
| 192 |
+
except Exception as e:
|
| 193 |
+
error_count += 1
|
| 194 |
+
error_msg = str(e)
|
| 195 |
+
controller.db_manager.update_account_status(
|
| 196 |
+
account['id'],
|
| 197 |
+
STATUS_FAILED,
|
| 198 |
+
notes=f"后台任务异常: {error_msg[:100]}"
|
| 199 |
+
)
|
| 200 |
+
controller.db_manager.log_operation(
|
| 201 |
+
account['username'],
|
| 202 |
+
"submit_email_background",
|
| 203 |
+
"error",
|
| 204 |
+
error_msg[:200]
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
tasks_list = [handle_account(account) for account in accounts]
|
| 208 |
+
await asyncio.gather(*tasks_list)
|
| 209 |
+
|
| 210 |
+
running_tasks[task_id] = {
|
| 211 |
+
"status": "completed",
|
| 212 |
+
"success_count": success_count,
|
| 213 |
+
"error_count": error_count,
|
| 214 |
+
"total": len(accounts)
|
| 215 |
+
}
|
| 216 |
+
except Exception as e:
|
| 217 |
+
logger.error(f"后台提交任务 {task_id} 执行失败: {str(e)}")
|
| 218 |
+
running_tasks[task_id] = {
|
| 219 |
+
"status": "failed",
|
| 220 |
+
"error": str(e)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
async def process_extract_task(task_id: str, account_ids: List[int], max_workers: int):
|
| 224 |
+
"""后台提取链接任务(并发版)"""
|
| 225 |
+
try:
|
| 226 |
+
# 获取要处理的账号
|
| 227 |
+
accounts = []
|
| 228 |
+
with controller.db_manager.lock:
|
| 229 |
+
cursor = controller.db_manager.conn.cursor()
|
| 230 |
+
if account_ids:
|
| 231 |
+
# 处理指定的账号
|
| 232 |
+
for account_id in account_ids:
|
| 233 |
+
cursor.execute(
|
| 234 |
+
"SELECT id, register_time, username, password, security_email, status, activation_link FROM accounts WHERE id = ?",
|
| 235 |
+
(account_id,)
|
| 236 |
+
)
|
| 237 |
+
row = cursor.fetchone()
|
| 238 |
+
if row and row[5] == STATUS_SUBMITTED: # 仅处理已提交的账号
|
| 239 |
+
accounts.append({
|
| 240 |
+
'id': row[0],
|
| 241 |
+
'register_time': row[1],
|
| 242 |
+
'username': row[2],
|
| 243 |
+
'password': row[3],
|
| 244 |
+
'security_email': row[4],
|
| 245 |
+
'status': row[5],
|
| 246 |
+
'activation_link': row[6]
|
| 247 |
+
})
|
| 248 |
+
else:
|
| 249 |
+
# 处理所有已提交但未提取链接的账号
|
| 250 |
+
cursor.execute(
|
| 251 |
+
"SELECT id, register_time, username, password, security_email, status, activation_link FROM accounts WHERE status = ? AND (activation_link IS NULL OR activation_link = '')",
|
| 252 |
+
(STATUS_SUBMITTED,)
|
| 253 |
+
)
|
| 254 |
+
for row in cursor.fetchall():
|
| 255 |
+
accounts.append({
|
| 256 |
+
'id': row[0],
|
| 257 |
+
'register_time': row[1],
|
| 258 |
+
'username': row[2],
|
| 259 |
+
'password': row[3],
|
| 260 |
+
'security_email': row[4],
|
| 261 |
+
'status': row[5],
|
| 262 |
+
'activation_link': row[6]
|
| 263 |
+
})
|
| 264 |
+
|
| 265 |
+
# 使用LinkExtractor提取链接
|
| 266 |
+
extractor = controller.extractor
|
| 267 |
+
success_count = 0
|
| 268 |
+
error_count = 0
|
| 269 |
+
sem = asyncio.Semaphore(max_workers)
|
| 270 |
+
|
| 271 |
+
async def handle_account(account):
|
| 272 |
+
nonlocal success_count, error_count
|
| 273 |
+
try:
|
| 274 |
+
async with sem:
|
| 275 |
+
controller.db_manager.log_operation(
|
| 276 |
+
account['username'],
|
| 277 |
+
"extract_link_background",
|
| 278 |
+
"processing",
|
| 279 |
+
f"任务ID: {task_id}"
|
| 280 |
+
)
|
| 281 |
+
loop = asyncio.get_running_loop()
|
| 282 |
+
link = await loop.run_in_executor(None, extractor.extract_link, account)
|
| 283 |
+
if link:
|
| 284 |
+
success_count += 1
|
| 285 |
+
else:
|
| 286 |
+
error_count += 1
|
| 287 |
+
await asyncio.sleep(1)
|
| 288 |
+
except Exception as e:
|
| 289 |
+
error_count += 1
|
| 290 |
+
error_msg = str(e)
|
| 291 |
+
controller.db_manager.update_account_status(
|
| 292 |
+
account['id'],
|
| 293 |
+
STATUS_SUBMITTED,
|
| 294 |
+
notes=f"后台任务异常: {error_msg[:100]}"
|
| 295 |
+
)
|
| 296 |
+
controller.db_manager.log_operation(
|
| 297 |
+
account['username'],
|
| 298 |
+
"extract_link_background",
|
| 299 |
+
"error",
|
| 300 |
+
error_msg[:200]
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
tasks_list = [handle_account(account) for account in accounts]
|
| 304 |
+
await asyncio.gather(*tasks_list)
|
| 305 |
+
|
| 306 |
+
running_tasks[task_id] = {
|
| 307 |
+
"status": "completed",
|
| 308 |
+
"success_count": success_count,
|
| 309 |
+
"error_count": error_count,
|
| 310 |
+
"total": len(accounts)
|
| 311 |
+
}
|
| 312 |
+
except Exception as e:
|
| 313 |
+
logger.error(f"后台提取任务 {task_id} 执行失败: {str(e)}")
|
| 314 |
+
running_tasks[task_id] = {
|
| 315 |
+
"status": "failed",
|
| 316 |
+
"error": str(e)
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
# API路由
|
| 320 |
+
@app.get("/")
|
| 321 |
+
async def root():
|
| 322 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 323 |
+
index_path = os.path.join(current_dir, "index.html")
|
| 324 |
+
if os.path.exists(index_path):
|
| 325 |
+
return FileResponse(index_path)
|
| 326 |
+
return {"message": "index.html not found in the current directory"}
|
| 327 |
+
|
| 328 |
+
@app.get("/accounts", response_model=AccountsResponse, dependencies=[Depends(verify_token)])
|
| 329 |
+
async def get_accounts(
|
| 330 |
+
status: Optional[int] = None,
|
| 331 |
+
page: int = Query(1, ge=1),
|
| 332 |
+
per_page: int = Query(10, ge=1, le=100),
|
| 333 |
+
search: Optional[str] = None,
|
| 334 |
+
register_time_start: Optional[str] = None,
|
| 335 |
+
register_time_end: Optional[str] = None,
|
| 336 |
+
id_min: Optional[int] = None,
|
| 337 |
+
id_max: Optional[int] = None,
|
| 338 |
+
has_activation_link: Optional[str] = None,
|
| 339 |
+
used: Optional[int] = Query(None)
|
| 340 |
+
):
|
| 341 |
+
"""获取账号列表,支持多条件筛选"""
|
| 342 |
+
offset = (page - 1) * per_page
|
| 343 |
+
try:
|
| 344 |
+
with controller.db_manager.lock:
|
| 345 |
+
cursor = controller.db_manager.conn.cursor()
|
| 346 |
+
query_conditions = []
|
| 347 |
+
query_params = []
|
| 348 |
+
if status is not None:
|
| 349 |
+
query_conditions.append("status = ?")
|
| 350 |
+
query_params.append(status)
|
| 351 |
+
if search:
|
| 352 |
+
query_conditions.append("(username LIKE ? OR security_email LIKE ? OR notes LIKE ? OR activation_link LIKE ?)")
|
| 353 |
+
search_param = f"%{search}%"
|
| 354 |
+
query_params.extend([search_param, search_param, search_param, search_param])
|
| 355 |
+
if register_time_start:
|
| 356 |
+
query_conditions.append("register_time >= ?")
|
| 357 |
+
query_params.append(register_time_start)
|
| 358 |
+
if register_time_end:
|
| 359 |
+
query_conditions.append("register_time <= ?")
|
| 360 |
+
query_params.append(register_time_end)
|
| 361 |
+
if id_min:
|
| 362 |
+
query_conditions.append("id >= ?")
|
| 363 |
+
query_params.append(id_min)
|
| 364 |
+
if id_max:
|
| 365 |
+
query_conditions.append("id <= ?")
|
| 366 |
+
query_params.append(id_max)
|
| 367 |
+
if has_activation_link == 'yes':
|
| 368 |
+
query_conditions.append("activation_link IS NOT NULL AND activation_link != ''")
|
| 369 |
+
elif has_activation_link == 'no':
|
| 370 |
+
query_conditions.append("(activation_link IS NULL OR activation_link = '')")
|
| 371 |
+
if used is not None and used != '':
|
| 372 |
+
query_conditions.append("used = ?")
|
| 373 |
+
query_params.append(int(used))
|
| 374 |
+
if query_conditions:
|
| 375 |
+
where_clause = "WHERE " + " AND ".join(query_conditions)
|
| 376 |
+
else:
|
| 377 |
+
where_clause = ""
|
| 378 |
+
count_query = f"SELECT COUNT(*) FROM accounts {where_clause}"
|
| 379 |
+
cursor.execute(count_query, query_params)
|
| 380 |
+
total = cursor.fetchone()[0]
|
| 381 |
+
data_query = f"""
|
| 382 |
+
SELECT id, register_time, username, password, security_email, status, activation_link, updated_at, notes, used
|
| 383 |
+
FROM accounts
|
| 384 |
+
{where_clause}
|
| 385 |
+
ORDER BY id DESC
|
| 386 |
+
LIMIT ? OFFSET ?
|
| 387 |
+
"""
|
| 388 |
+
query_params_data = query_params + [per_page, offset]
|
| 389 |
+
cursor.execute(data_query, query_params_data)
|
| 390 |
+
accounts = []
|
| 391 |
+
for row in cursor.fetchall():
|
| 392 |
+
accounts.append(AccountItem(
|
| 393 |
+
id=row[0],
|
| 394 |
+
register_time=row[1],
|
| 395 |
+
username=row[2],
|
| 396 |
+
password=row[3],
|
| 397 |
+
security_email=row[4],
|
| 398 |
+
status=row[5],
|
| 399 |
+
activation_link=row[6],
|
| 400 |
+
updated_at=row[7],
|
| 401 |
+
notes=row[8],
|
| 402 |
+
used=row[9] if len(row) > 9 else 0
|
| 403 |
+
))
|
| 404 |
+
return AccountsResponse(accounts=accounts, total=total)
|
| 405 |
+
except Exception as e:
|
| 406 |
+
logger.error(f"获取账号列表出错: {str(e)}")
|
| 407 |
+
raise HTTPException(status_code=500, detail=f"获取账号列表出错: {str(e)}")
|
| 408 |
+
|
| 409 |
+
@app.get("/accounts/{account_id}", response_model=AccountItem, dependencies=[Depends(verify_token)])
|
| 410 |
+
async def get_account(account_id: int):
|
| 411 |
+
"""获取单个账号详情"""
|
| 412 |
+
try:
|
| 413 |
+
with controller.db_manager.lock:
|
| 414 |
+
cursor = controller.db_manager.conn.cursor()
|
| 415 |
+
cursor.execute(
|
| 416 |
+
"SELECT id, register_time, username, password, security_email, status, activation_link, updated_at, notes FROM accounts WHERE id = ?",
|
| 417 |
+
(account_id,)
|
| 418 |
+
)
|
| 419 |
+
row = cursor.fetchone()
|
| 420 |
+
|
| 421 |
+
if not row:
|
| 422 |
+
raise HTTPException(status_code=404, detail=f"未找到ID为{account_id}的账号")
|
| 423 |
+
|
| 424 |
+
return AccountItem(
|
| 425 |
+
id=row[0],
|
| 426 |
+
register_time=row[1],
|
| 427 |
+
username=row[2],
|
| 428 |
+
password=row[3],
|
| 429 |
+
security_email=row[4],
|
| 430 |
+
status=row[5],
|
| 431 |
+
activation_link=row[6],
|
| 432 |
+
updated_at=row[7],
|
| 433 |
+
notes=row[8]
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
except HTTPException:
|
| 437 |
+
raise
|
| 438 |
+
except Exception as e:
|
| 439 |
+
logger.error(f"获取账号详情出错: {str(e)}")
|
| 440 |
+
raise HTTPException(status_code=500, detail=f"获取账号详情出错: {str(e)}")
|
| 441 |
+
|
| 442 |
+
@app.post("/accounts/{account_id}/status", response_model=AccountItem, dependencies=[Depends(verify_token)])
|
| 443 |
+
async def update_account_status(account_id: int, status_update: StatusUpdateRequest):
|
| 444 |
+
"""更新账号状态"""
|
| 445 |
+
try:
|
| 446 |
+
username = None
|
| 447 |
+
updated_account = None
|
| 448 |
+
|
| 449 |
+
# 使用锁进行数据库操作
|
| 450 |
+
with controller.db_manager.lock:
|
| 451 |
+
cursor = controller.db_manager.conn.cursor()
|
| 452 |
+
|
| 453 |
+
# 先检查账号是否存在
|
| 454 |
+
cursor.execute("SELECT username FROM accounts WHERE id = ?", (account_id,))
|
| 455 |
+
result = cursor.fetchone()
|
| 456 |
+
if not result:
|
| 457 |
+
raise HTTPException(status_code=404, detail=f"未找到ID为{account_id}的账号")
|
| 458 |
+
|
| 459 |
+
username = result[0]
|
| 460 |
+
|
| 461 |
+
# 直接在锁内执行更新操作,避免嵌套锁
|
| 462 |
+
if status_update.activation_link and status_update.notes:
|
| 463 |
+
cursor.execute('''
|
| 464 |
+
UPDATE accounts
|
| 465 |
+
SET status = ?, activation_link = ?, notes = ?, updated_at = datetime('now', 'localtime')
|
| 466 |
+
WHERE id = ?
|
| 467 |
+
''', (status_update.status, status_update.activation_link, status_update.notes, account_id))
|
| 468 |
+
elif status_update.activation_link:
|
| 469 |
+
cursor.execute('''
|
| 470 |
+
UPDATE accounts
|
| 471 |
+
SET status = ?, activation_link = ?, updated_at = datetime('now', 'localtime')
|
| 472 |
+
WHERE id = ?
|
| 473 |
+
''', (status_update.status, status_update.activation_link, account_id))
|
| 474 |
+
elif status_update.notes:
|
| 475 |
+
cursor.execute('''
|
| 476 |
+
UPDATE accounts
|
| 477 |
+
SET status = ?, notes = ?, updated_at = datetime('now', 'localtime')
|
| 478 |
+
WHERE id = ?
|
| 479 |
+
''', (status_update.status, status_update.notes, account_id))
|
| 480 |
+
else:
|
| 481 |
+
cursor.execute('''
|
| 482 |
+
UPDATE accounts
|
| 483 |
+
SET status = ?, updated_at = datetime('now', 'localtime')
|
| 484 |
+
WHERE id = ?
|
| 485 |
+
''', (status_update.status, account_id))
|
| 486 |
+
|
| 487 |
+
# 提交事务
|
| 488 |
+
controller.db_manager.conn.commit()
|
| 489 |
+
|
| 490 |
+
# 返回更新后的账号
|
| 491 |
+
cursor.execute(
|
| 492 |
+
"SELECT id, register_time, username, password, security_email, status, activation_link, updated_at, notes FROM accounts WHERE id = ?",
|
| 493 |
+
(account_id,)
|
| 494 |
+
)
|
| 495 |
+
row = cursor.fetchone()
|
| 496 |
+
|
| 497 |
+
updated_account = AccountItem(
|
| 498 |
+
id=row[0],
|
| 499 |
+
register_time=row[1],
|
| 500 |
+
username=row[2],
|
| 501 |
+
password=row[3],
|
| 502 |
+
security_email=row[4],
|
| 503 |
+
status=row[5],
|
| 504 |
+
activation_link=row[6],
|
| 505 |
+
updated_at=row[7],
|
| 506 |
+
notes=row[8]
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
# 锁外记录操作日志,避免死锁
|
| 510 |
+
if username:
|
| 511 |
+
controller.db_manager.log_operation(
|
| 512 |
+
username,
|
| 513 |
+
"update_status",
|
| 514 |
+
"success",
|
| 515 |
+
f"手动更新状态为: {status_update.status}"
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
return updated_account
|
| 519 |
+
|
| 520 |
+
except HTTPException:
|
| 521 |
+
raise
|
| 522 |
+
except Exception as e:
|
| 523 |
+
logger.error(f"更新账号状态出错: {str(e)}")
|
| 524 |
+
raise HTTPException(status_code=500, detail=f"更新账号状态出错: {str(e)}")
|
| 525 |
+
|
| 526 |
+
@app.post("/accounts/{account_id}/toggle-used", dependencies=[Depends(verify_token)])
|
| 527 |
+
async def toggle_account_used(account_id: int):
|
| 528 |
+
"""切换账号的used状态"""
|
| 529 |
+
try:
|
| 530 |
+
with controller.db_manager.lock:
|
| 531 |
+
cursor = controller.db_manager.conn.cursor()
|
| 532 |
+
cursor.execute("SELECT used FROM accounts WHERE id = ?", (account_id,))
|
| 533 |
+
row = cursor.fetchone()
|
| 534 |
+
if not row:
|
| 535 |
+
raise HTTPException(status_code=404, detail=f"未找到ID为{account_id}的账号")
|
| 536 |
+
current = row[0] or 0
|
| 537 |
+
new_value = 0 if current else 1
|
| 538 |
+
cursor.execute("UPDATE accounts SET used = ?, updated_at = datetime('now', 'localtime') WHERE id = ?", (new_value, account_id))
|
| 539 |
+
controller.db_manager.conn.commit()
|
| 540 |
+
return {"id": account_id, "used": new_value}
|
| 541 |
+
except Exception as e:
|
| 542 |
+
logger.error(f"切换账号used状态出错: {str(e)}")
|
| 543 |
+
raise HTTPException(status_code=500, detail=f"切换账号used状态出错: {str(e)}")
|
| 544 |
+
|
| 545 |
+
@app.post("/accounts/batch-toggle-used", dependencies=[Depends(verify_token)])
|
| 546 |
+
async def batch_toggle_used(req: BatchToggleUsedRequest = Body(...)):
|
| 547 |
+
"""批量修改账号的used状态"""
|
| 548 |
+
try:
|
| 549 |
+
with controller.db_manager.lock:
|
| 550 |
+
cursor = controller.db_manager.conn.cursor()
|
| 551 |
+
if not req.ids:
|
| 552 |
+
raise HTTPException(status_code=400, detail="未指定账号ID")
|
| 553 |
+
cursor.execute(f"UPDATE accounts SET used = ?, updated_at = datetime('now', 'localtime') WHERE id IN ({','.join(['?']*len(req.ids))})", [req.used, *req.ids])
|
| 554 |
+
controller.db_manager.conn.commit()
|
| 555 |
+
return {"updated": cursor.rowcount}
|
| 556 |
+
except Exception as e:
|
| 557 |
+
logger.error(f"批量切换账号used状态出错: {str(e)}")
|
| 558 |
+
raise HTTPException(status_code=500, detail=f"批量切换账号used状态出错: {str(e)}")
|
| 559 |
+
|
| 560 |
+
@app.delete("/accounts/{account_id}", dependencies=[Depends(verify_token)])
|
| 561 |
+
async def delete_account(account_id: int):
|
| 562 |
+
logger.info(f"收到删除账号请求: {account_id}")
|
| 563 |
+
try:
|
| 564 |
+
with controller.db_manager.lock:
|
| 565 |
+
logger.info(f"已获取数据库锁,准备查询账号: {account_id}")
|
| 566 |
+
cursor = controller.db_manager.conn.cursor()
|
| 567 |
+
cursor.execute("SELECT username FROM accounts WHERE id = ?", (account_id,))
|
| 568 |
+
row = cursor.fetchone()
|
| 569 |
+
if not row:
|
| 570 |
+
logger.warning(f"未找到账号: {account_id}")
|
| 571 |
+
raise HTTPException(status_code=404, detail=f"未找到ID为{account_id}的账号")
|
| 572 |
+
username = row[0]
|
| 573 |
+
logger.info(f"准备删除账号: {account_id}, 用户名: {username}")
|
| 574 |
+
cursor.execute("DELETE FROM accounts WHERE id = ?", (account_id,))
|
| 575 |
+
controller.db_manager.conn.commit()
|
| 576 |
+
logger.info(f"账号已删除: {account_id}")
|
| 577 |
+
# 注意:操作日志写入要放到锁外,避免死锁
|
| 578 |
+
controller.db_manager.log_operation(username, "delete_account", "success", f"删除账号ID: {account_id}")
|
| 579 |
+
logger.info(f"删除账号流程结束: {account_id}")
|
| 580 |
+
return {"message": f"账号 {account_id} 已删除"}
|
| 581 |
+
except HTTPException:
|
| 582 |
+
raise
|
| 583 |
+
except Exception as e:
|
| 584 |
+
logger.error(f"删除账号出错: {str(e)}")
|
| 585 |
+
raise HTTPException(status_code=500, detail=f"删除账号出错: {str(e)}")
|
| 586 |
+
|
| 587 |
+
@app.post("/submit", response_model=Dict, dependencies=[Depends(verify_token)])
|
| 588 |
+
async def submit_emails(request: SubmitRequest, background_tasks: BackgroundTasks):
|
| 589 |
+
"""提交邮箱到JetBrains(异步任务)"""
|
| 590 |
+
try:
|
| 591 |
+
task_id = f"submit_{datetime.now().strftime('%Y%m%d%H%M%S')}_{len(running_tasks) + 1}"
|
| 592 |
+
|
| 593 |
+
# 设置初始任务状态
|
| 594 |
+
running_tasks[task_id] = {
|
| 595 |
+
"status": "running",
|
| 596 |
+
"type": "submit",
|
| 597 |
+
"start_time": datetime.now().isoformat(),
|
| 598 |
+
"account_ids": request.account_ids,
|
| 599 |
+
"max_workers": request.max_workers
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
# 启动后台任务
|
| 603 |
+
background_tasks.add_task(
|
| 604 |
+
process_submit_task,
|
| 605 |
+
task_id=task_id,
|
| 606 |
+
account_ids=request.account_ids,
|
| 607 |
+
max_workers=request.max_workers,
|
| 608 |
+
proxy=request.proxy
|
| 609 |
+
)
|
| 610 |
+
|
| 611 |
+
return {
|
| 612 |
+
"task_id": task_id,
|
| 613 |
+
"message": "邮箱提交任务已启动",
|
| 614 |
+
"status": "running"
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
except Exception as e:
|
| 618 |
+
logger.error(f"启动提交任务出错: {str(e)}")
|
| 619 |
+
raise HTTPException(status_code=500, detail=f"启动提交任务出错: {str(e)}")
|
| 620 |
+
|
| 621 |
+
@app.post("/extract", response_model=Dict, dependencies=[Depends(verify_token)])
|
| 622 |
+
async def extract_links(request: ExtractRequest, background_tasks: BackgroundTasks):
|
| 623 |
+
"""从邮箱提取激活链接(异步任务)"""
|
| 624 |
+
try:
|
| 625 |
+
task_id = f"extract_{datetime.now().strftime('%Y%m%d%H%M%S')}_{len(running_tasks) + 1}"
|
| 626 |
+
|
| 627 |
+
# 设置初始任务状态
|
| 628 |
+
running_tasks[task_id] = {
|
| 629 |
+
"status": "running",
|
| 630 |
+
"type": "extract",
|
| 631 |
+
"start_time": datetime.now().isoformat(),
|
| 632 |
+
"account_ids": request.account_ids,
|
| 633 |
+
"max_workers": request.max_workers
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
# 启动后台任务
|
| 637 |
+
background_tasks.add_task(
|
| 638 |
+
process_extract_task,
|
| 639 |
+
task_id=task_id,
|
| 640 |
+
account_ids=request.account_ids,
|
| 641 |
+
max_workers=request.max_workers
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
return {
|
| 645 |
+
"task_id": task_id,
|
| 646 |
+
"message": "链接提取任务已启动",
|
| 647 |
+
"status": "running"
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
except Exception as e:
|
| 651 |
+
logger.error(f"启动提取任务出错: {str(e)}")
|
| 652 |
+
raise HTTPException(status_code=500, detail=f"启动提取任务出错: {str(e)}")
|
| 653 |
+
|
| 654 |
+
@app.get("/tasks/{task_id}", dependencies=[Depends(verify_token)])
|
| 655 |
+
async def get_task_status(task_id: str):
|
| 656 |
+
"""获取任务状态"""
|
| 657 |
+
if task_id not in running_tasks:
|
| 658 |
+
raise HTTPException(status_code=404, detail=f"未找到任务ID: {task_id}")
|
| 659 |
+
|
| 660 |
+
return running_tasks[task_id]
|
| 661 |
+
|
| 662 |
+
@app.get("/tasks", dependencies=[Depends(verify_token)])
|
| 663 |
+
async def get_all_tasks():
|
| 664 |
+
"""获取所有任务状态"""
|
| 665 |
+
return running_tasks
|
| 666 |
+
|
| 667 |
+
@app.post("/import", response_model=ImportResponse, dependencies=[Depends(verify_token)])
|
| 668 |
+
async def import_accounts(file: UploadFile = File(...)):
|
| 669 |
+
"""从上传的文件导入账号"""
|
| 670 |
+
try:
|
| 671 |
+
content = await file.read()
|
| 672 |
+
# 假设文件是utf-8编码文本
|
| 673 |
+
text = content.decode("utf-8")
|
| 674 |
+
# 将内容写入临时文件,再用原有的import_data逻辑导入
|
| 675 |
+
import tempfile
|
| 676 |
+
with tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf-8", suffix=".txt") as tmp:
|
| 677 |
+
tmp.write(text)
|
| 678 |
+
tmp_path = tmp.name
|
| 679 |
+
imported_count = controller.import_data(tmp_path)
|
| 680 |
+
# 获取总账号数
|
| 681 |
+
with controller.db_manager.lock:
|
| 682 |
+
cursor = controller.db_manager.conn.cursor()
|
| 683 |
+
cursor.execute("SELECT COUNT(*) FROM accounts")
|
| 684 |
+
total = cursor.fetchone()[0]
|
| 685 |
+
# 删除临时文件
|
| 686 |
+
import os
|
| 687 |
+
os.remove(tmp_path)
|
| 688 |
+
return ImportResponse(
|
| 689 |
+
imported_count=imported_count,
|
| 690 |
+
total_accounts=total
|
| 691 |
+
)
|
| 692 |
+
except HTTPException:
|
| 693 |
+
raise
|
| 694 |
+
except Exception as e:
|
| 695 |
+
logger.error(f"导入账号出错: {str(e)}")
|
| 696 |
+
raise HTTPException(status_code=500, detail=f"导入账号出错: {str(e)}")
|
| 697 |
+
|
| 698 |
+
@app.post("/export", dependencies=[Depends(verify_token)])
|
| 699 |
+
async def export_accounts(request: dict = Body(...)):
|
| 700 |
+
"""导出账号到文件并下载到本地,支持所有筛选条件"""
|
| 701 |
+
try:
|
| 702 |
+
import io
|
| 703 |
+
buffer = io.StringIO()
|
| 704 |
+
# 动态拼接筛选条件
|
| 705 |
+
query_conditions = []
|
| 706 |
+
query_params = []
|
| 707 |
+
status = request.get('status', None)
|
| 708 |
+
used = request.get('used', None)
|
| 709 |
+
search = request.get('search', None)
|
| 710 |
+
has_activation_link = request.get('has_activation_link', None)
|
| 711 |
+
id_min = request.get('id_min', None)
|
| 712 |
+
id_max = request.get('id_max', None)
|
| 713 |
+
register_time_start = request.get('register_time_start', None)
|
| 714 |
+
register_time_end = request.get('register_time_end', None)
|
| 715 |
+
if status is not None and status != '':
|
| 716 |
+
query_conditions.append("status = ?")
|
| 717 |
+
query_params.append(status)
|
| 718 |
+
if search:
|
| 719 |
+
query_conditions.append("(username LIKE ? OR security_email LIKE ? OR notes LIKE ? OR activation_link LIKE ?)")
|
| 720 |
+
search_param = f"%{search}%"
|
| 721 |
+
query_params.extend([search_param, search_param, search_param, search_param])
|
| 722 |
+
if register_time_start:
|
| 723 |
+
query_conditions.append("register_time >= ?")
|
| 724 |
+
query_params.append(register_time_start)
|
| 725 |
+
if register_time_end:
|
| 726 |
+
query_conditions.append("register_time <= ?")
|
| 727 |
+
query_params.append(register_time_end)
|
| 728 |
+
if id_min:
|
| 729 |
+
query_conditions.append("id >= ?")
|
| 730 |
+
query_params.append(id_min)
|
| 731 |
+
if id_max:
|
| 732 |
+
query_conditions.append("id <= ?")
|
| 733 |
+
query_params.append(id_max)
|
| 734 |
+
if has_activation_link == 'yes':
|
| 735 |
+
query_conditions.append("activation_link IS NOT NULL AND activation_link != ''")
|
| 736 |
+
elif has_activation_link == 'no':
|
| 737 |
+
query_conditions.append("(activation_link IS NULL OR activation_link = '')")
|
| 738 |
+
if used is not None and used != '':
|
| 739 |
+
query_conditions.append("used = ?")
|
| 740 |
+
query_params.append(int(used))
|
| 741 |
+
if query_conditions:
|
| 742 |
+
where_clause = "WHERE " + " AND ".join(query_conditions)
|
| 743 |
+
else:
|
| 744 |
+
where_clause = ""
|
| 745 |
+
with controller.db_manager.lock:
|
| 746 |
+
cursor = controller.db_manager.conn.cursor()
|
| 747 |
+
cursor.execute(f"""
|
| 748 |
+
SELECT activation_link
|
| 749 |
+
FROM accounts
|
| 750 |
+
{where_clause}
|
| 751 |
+
ORDER BY id DESC
|
| 752 |
+
""", query_params)
|
| 753 |
+
for row in cursor.fetchall():
|
| 754 |
+
if row[0]:
|
| 755 |
+
buffer.write(f"{row[0]}\n")
|
| 756 |
+
buffer.seek(0)
|
| 757 |
+
content = buffer.getvalue().encode('utf-8')
|
| 758 |
+
byte_io = io.BytesIO(content)
|
| 759 |
+
filename = request.get('filename', 'export_accounts.txt')
|
| 760 |
+
headers = {
|
| 761 |
+
'Content-Disposition': f'attachment; filename="{filename}"'
|
| 762 |
+
}
|
| 763 |
+
return StreamingResponse(
|
| 764 |
+
byte_io,
|
| 765 |
+
media_type="text/plain",
|
| 766 |
+
headers=headers
|
| 767 |
+
)
|
| 768 |
+
except Exception as e:
|
| 769 |
+
logger.error(f"导出账号出错: {str(e)}")
|
| 770 |
+
raise HTTPException(status_code=500, detail=f"导出账号出错: {str(e)}")
|
| 771 |
+
|
| 772 |
+
@app.get("/statistics", response_model=StatusCounts, dependencies=[Depends(verify_token)])
|
| 773 |
+
async def get_statistics():
|
| 774 |
+
"""获取账号统计信息"""
|
| 775 |
+
try:
|
| 776 |
+
with controller.db_manager.lock:
|
| 777 |
+
cursor = controller.db_manager.conn.cursor()
|
| 778 |
+
|
| 779 |
+
# 获取总账号数
|
| 780 |
+
cursor.execute("SELECT COUNT(*) FROM accounts")
|
| 781 |
+
total = cursor.fetchone()[0]
|
| 782 |
+
|
| 783 |
+
# 获取各状态账号数
|
| 784 |
+
cursor.execute("SELECT COUNT(*) FROM accounts WHERE status = ?", (STATUS_PENDING,))
|
| 785 |
+
pending = cursor.fetchone()[0]
|
| 786 |
+
|
| 787 |
+
cursor.execute("SELECT COUNT(*) FROM accounts WHERE status = ?", (STATUS_SUBMITTED,))
|
| 788 |
+
submitted = cursor.fetchone()[0]
|
| 789 |
+
|
| 790 |
+
cursor.execute("SELECT COUNT(*) FROM accounts WHERE status = ?", (STATUS_FAILED,))
|
| 791 |
+
failed = cursor.fetchone()[0]
|
| 792 |
+
|
| 793 |
+
cursor.execute("SELECT COUNT(*) FROM accounts WHERE status = ?", (STATUS_LINK_EXTRACTED,))
|
| 794 |
+
link_extracted = cursor.fetchone()[0]
|
| 795 |
+
|
| 796 |
+
cursor.execute("SELECT COUNT(*) FROM accounts WHERE used = 0")
|
| 797 |
+
unused = cursor.fetchone()[0]
|
| 798 |
+
|
| 799 |
+
return StatusCounts(
|
| 800 |
+
total=total,
|
| 801 |
+
pending=pending,
|
| 802 |
+
submitted=submitted,
|
| 803 |
+
failed=failed,
|
| 804 |
+
link_extracted=link_extracted,
|
| 805 |
+
unused=unused
|
| 806 |
+
)
|
| 807 |
+
|
| 808 |
+
except Exception as e:
|
| 809 |
+
logger.error(f"获取统计信息出错: {str(e)}")
|
| 810 |
+
raise HTTPException(status_code=500, detail=f"获取统计信息出错: {str(e)}")
|
| 811 |
+
|
| 812 |
+
@app.post("/reset-failed", dependencies=[Depends(verify_token)])
|
| 813 |
+
async def reset_failed_accounts():
|
| 814 |
+
"""将失败的账号状态重置为未提交状态"""
|
| 815 |
+
try:
|
| 816 |
+
with controller.db_manager.lock:
|
| 817 |
+
cursor = controller.db_manager.conn.cursor()
|
| 818 |
+
cursor.execute(
|
| 819 |
+
"UPDATE accounts SET status = ?, notes = 'Reset from failed status', updated_at = datetime('now', 'localtime') WHERE status = ?",
|
| 820 |
+
(STATUS_PENDING, STATUS_FAILED)
|
| 821 |
+
)
|
| 822 |
+
controller.db_manager.conn.commit()
|
| 823 |
+
|
| 824 |
+
# 获取重置的账号数
|
| 825 |
+
reset_count = cursor.rowcount
|
| 826 |
+
|
| 827 |
+
return {"reset_count": reset_count, "message": f"已将{reset_count}个失败账号重置为未提交状态"}
|
| 828 |
+
|
| 829 |
+
except Exception as e:
|
| 830 |
+
logger.error(f"重置失败账号出错: {str(e)}")
|
| 831 |
+
raise HTTPException(status_code=500, detail=f"重置失败账号出错: {str(e)}")
|
| 832 |
+
|
| 833 |
+
@app.get("/logs", dependencies=[Depends(verify_token)])
|
| 834 |
+
async def get_logs(
|
| 835 |
+
username: Optional[str] = None,
|
| 836 |
+
operation: Optional[str] = None,
|
| 837 |
+
status: Optional[str] = None,
|
| 838 |
+
page: int = Query(1, ge=1),
|
| 839 |
+
per_page: int = Query(20, ge=1, le=100)
|
| 840 |
+
):
|
| 841 |
+
"""获取操作日志"""
|
| 842 |
+
offset = (page - 1) * per_page
|
| 843 |
+
|
| 844 |
+
try:
|
| 845 |
+
with controller.db_manager.lock:
|
| 846 |
+
cursor = controller.db_manager.conn.cursor()
|
| 847 |
+
|
| 848 |
+
# 构建查询条件
|
| 849 |
+
query_conditions = []
|
| 850 |
+
query_params = []
|
| 851 |
+
|
| 852 |
+
if username:
|
| 853 |
+
query_conditions.append("username LIKE ?")
|
| 854 |
+
query_params.append(f"%{username}%")
|
| 855 |
+
|
| 856 |
+
if operation:
|
| 857 |
+
query_conditions.append("operation = ?")
|
| 858 |
+
query_params.append(operation)
|
| 859 |
+
|
| 860 |
+
if status:
|
| 861 |
+
query_conditions.append("status = ?")
|
| 862 |
+
query_params.append(status)
|
| 863 |
+
|
| 864 |
+
# 构建完整查询
|
| 865 |
+
if query_conditions:
|
| 866 |
+
where_clause = "WHERE " + " AND ".join(query_conditions)
|
| 867 |
+
else:
|
| 868 |
+
where_clause = ""
|
| 869 |
+
|
| 870 |
+
# 计算总记录数
|
| 871 |
+
count_query = f"SELECT COUNT(*) FROM operation_logs {where_clause}"
|
| 872 |
+
cursor.execute(count_query, query_params)
|
| 873 |
+
total = cursor.fetchone()[0]
|
| 874 |
+
|
| 875 |
+
# 获取分页数据
|
| 876 |
+
data_query = f"""
|
| 877 |
+
SELECT id, username, operation, status, message, created_at
|
| 878 |
+
FROM operation_logs
|
| 879 |
+
{where_clause}
|
| 880 |
+
ORDER BY id DESC
|
| 881 |
+
LIMIT ? OFFSET ?
|
| 882 |
+
"""
|
| 883 |
+
|
| 884 |
+
# 添加分页参数
|
| 885 |
+
query_params.extend([per_page, offset])
|
| 886 |
+
cursor.execute(data_query, query_params)
|
| 887 |
+
|
| 888 |
+
logs = []
|
| 889 |
+
for row in cursor.fetchall():
|
| 890 |
+
logs.append({
|
| 891 |
+
"id": row[0],
|
| 892 |
+
"username": row[1],
|
| 893 |
+
"operation": row[2],
|
| 894 |
+
"status": row[3],
|
| 895 |
+
"message": row[4],
|
| 896 |
+
"created_at": row[5]
|
| 897 |
+
})
|
| 898 |
+
|
| 899 |
+
return {"logs": logs, "total": total}
|
| 900 |
+
|
| 901 |
+
except Exception as e:
|
| 902 |
+
logger.error(f"获取操作日志出错: {str(e)}")
|
| 903 |
+
raise HTTPException(status_code=500, detail=f"获取操作日志出错: {str(e)}")
|
| 904 |
+
|
| 905 |
+
@app.post("/login")
|
| 906 |
+
async def login(data: dict = Body(...)):
|
| 907 |
+
password = data.get("password", "")
|
| 908 |
+
if password == ADMIN_PASSWORD:
|
| 909 |
+
return {"token": ADMIN_PASSWORD}
|
| 910 |
+
else:
|
| 911 |
+
raise HTTPException(status_code=401, detail="密码错误")
|
| 912 |
+
|
| 913 |
+
if __name__ == "__main__":
|
| 914 |
+
# 运行FastAPI应用
|
| 915 |
+
uvicorn.run("api_server:app", host="0.0.0.0", port=8000, reload=True)
|
index.html
ADDED
|
@@ -0,0 +1,1606 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>JetBrains激活链接管理系统</title>
|
| 7 |
+
<!-- 使用CDN引入Vue 3和Element Plus,指定固定版本 -->
|
| 8 |
+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/index.css" />
|
| 9 |
+
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>
|
| 10 |
+
<script src="https://unpkg.com/[email protected]"></script>
|
| 11 |
+
<script src="https://unpkg.com/@element-plus/[email protected]"></script>
|
| 12 |
+
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
|
| 13 |
+
<style>
|
| 14 |
+
body {
|
| 15 |
+
margin: 0;
|
| 16 |
+
padding: 0;
|
| 17 |
+
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
|
| 18 |
+
-webkit-font-smoothing: antialiased;
|
| 19 |
+
-moz-osx-font-smoothing: grayscale;
|
| 20 |
+
background-color: #f5f7fa;
|
| 21 |
+
color: #303133;
|
| 22 |
+
}
|
| 23 |
+
.app-container {
|
| 24 |
+
padding: 8px;
|
| 25 |
+
}
|
| 26 |
+
.dashboard-card {
|
| 27 |
+
margin-bottom: 20px;
|
| 28 |
+
border-radius: 4px;
|
| 29 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 30 |
+
background-color: #fff;
|
| 31 |
+
}
|
| 32 |
+
.dashboard-header {
|
| 33 |
+
display: flex;
|
| 34 |
+
align-items: center;
|
| 35 |
+
justify-content: space-between;
|
| 36 |
+
padding: 10px 20px;
|
| 37 |
+
border-bottom: 1px solid #ebeef5;
|
| 38 |
+
}
|
| 39 |
+
.el-table .status-column {
|
| 40 |
+
display: flex;
|
| 41 |
+
align-items: center;
|
| 42 |
+
justify-content: center;
|
| 43 |
+
}
|
| 44 |
+
.el-table .status-0 {
|
| 45 |
+
color: #909399;
|
| 46 |
+
}
|
| 47 |
+
.el-table .status-1 {
|
| 48 |
+
color: #409EFF;
|
| 49 |
+
}
|
| 50 |
+
.el-table .status-2 {
|
| 51 |
+
color: #F56C6C;
|
| 52 |
+
}
|
| 53 |
+
.el-table .status-3 {
|
| 54 |
+
color: #67C23A;
|
| 55 |
+
}
|
| 56 |
+
.operation-btn {
|
| 57 |
+
margin-right: 5px;
|
| 58 |
+
}
|
| 59 |
+
.statistics-card {
|
| 60 |
+
text-align: center;
|
| 61 |
+
padding: 20px;
|
| 62 |
+
}
|
| 63 |
+
.statistics-value {
|
| 64 |
+
font-size: 36px;
|
| 65 |
+
font-weight: bold;
|
| 66 |
+
}
|
| 67 |
+
.statistics-label {
|
| 68 |
+
font-size: 14px;
|
| 69 |
+
color: #909399;
|
| 70 |
+
}
|
| 71 |
+
.task-card {
|
| 72 |
+
margin-bottom: 15px;
|
| 73 |
+
}
|
| 74 |
+
.copy-button {
|
| 75 |
+
margin-left: 5px;
|
| 76 |
+
}
|
| 77 |
+
.el-tag {
|
| 78 |
+
margin: 2px;
|
| 79 |
+
}
|
| 80 |
+
.tab-container {
|
| 81 |
+
padding: 15px;
|
| 82 |
+
}
|
| 83 |
+
.sr-only {
|
| 84 |
+
position: absolute;
|
| 85 |
+
width: 1px;
|
| 86 |
+
height: 1px;
|
| 87 |
+
padding: 0;
|
| 88 |
+
margin: -1px;
|
| 89 |
+
overflow: hidden;
|
| 90 |
+
clip: rect(0, 0, 0, 0);
|
| 91 |
+
white-space: nowrap;
|
| 92 |
+
border-width: 0;
|
| 93 |
+
}
|
| 94 |
+
.required-field::after {
|
| 95 |
+
content: ' *';
|
| 96 |
+
color: #F56C6C;
|
| 97 |
+
}
|
| 98 |
+
.el-table .id-column,
|
| 99 |
+
.el-table .register-time-column,
|
| 100 |
+
.el-table .username-column,
|
| 101 |
+
.el-table .email-column {
|
| 102 |
+
white-space: nowrap;
|
| 103 |
+
overflow: hidden;
|
| 104 |
+
text-overflow: ellipsis;
|
| 105 |
+
}
|
| 106 |
+
.el-table .email-column {
|
| 107 |
+
max-width: 250px;
|
| 108 |
+
}
|
| 109 |
+
/* 调试样式 - 显示表格边框和背景色 */
|
| 110 |
+
.el-table {
|
| 111 |
+
border: 1px solid #dcdfe6;
|
| 112 |
+
}
|
| 113 |
+
.el-table th {
|
| 114 |
+
background-color: #f5f7fa !important;
|
| 115 |
+
color: #606266;
|
| 116 |
+
font-weight: bold;
|
| 117 |
+
text-align: center !important;
|
| 118 |
+
}
|
| 119 |
+
.el-table td {
|
| 120 |
+
text-align: center;
|
| 121 |
+
}
|
| 122 |
+
/* 确保ID列和时间列有足够的宽度 */
|
| 123 |
+
.el-table .id-column {
|
| 124 |
+
min-width: 80px !important;
|
| 125 |
+
width: 80px !important;
|
| 126 |
+
}
|
| 127 |
+
.el-table .register-time-column {
|
| 128 |
+
min-width: 180px !important;
|
| 129 |
+
width: 180px !important;
|
| 130 |
+
}
|
| 131 |
+
.el-table .username-column {
|
| 132 |
+
min-width: 250px !important;
|
| 133 |
+
}
|
| 134 |
+
.el-table .email-column {
|
| 135 |
+
min-width: 180px !important;
|
| 136 |
+
}
|
| 137 |
+
/* 必填字段标记 */
|
| 138 |
+
.required::after {
|
| 139 |
+
content: '*';
|
| 140 |
+
color: #F56C6C;
|
| 141 |
+
margin-left: 4px;
|
| 142 |
+
}
|
| 143 |
+
@media (max-width: 900px) {
|
| 144 |
+
.app-container {
|
| 145 |
+
padding: 2px !important;
|
| 146 |
+
}
|
| 147 |
+
.dashboard-card {
|
| 148 |
+
margin-bottom: 8px;
|
| 149 |
+
padding: 0 2px;
|
| 150 |
+
}
|
| 151 |
+
.statistics-card {
|
| 152 |
+
border-radius: 10px;
|
| 153 |
+
box-shadow: 0 2px 8px #eee;
|
| 154 |
+
padding: 10px 0;
|
| 155 |
+
text-align: center !important;
|
| 156 |
+
display: flex;
|
| 157 |
+
flex-direction: column;
|
| 158 |
+
align-items: center;
|
| 159 |
+
justify-content: center;
|
| 160 |
+
}
|
| 161 |
+
.statistics-value {
|
| 162 |
+
font-size: 28px;
|
| 163 |
+
text-align: center !important;
|
| 164 |
+
width: 100%;
|
| 165 |
+
margin: 0 auto;
|
| 166 |
+
}
|
| 167 |
+
.statistics-label {
|
| 168 |
+
font-size: 16px;
|
| 169 |
+
text-align: center !important;
|
| 170 |
+
width: 100%;
|
| 171 |
+
margin: 0 auto;
|
| 172 |
+
}
|
| 173 |
+
.tab-container {
|
| 174 |
+
padding: 4px;
|
| 175 |
+
}
|
| 176 |
+
.el-row.filter-row {
|
| 177 |
+
margin: 0 -2px;
|
| 178 |
+
}
|
| 179 |
+
.el-col.filter-col {
|
| 180 |
+
margin-bottom: 4px;
|
| 181 |
+
padding: 0 2px;
|
| 182 |
+
}
|
| 183 |
+
.el-table {
|
| 184 |
+
border-radius: 6px;
|
| 185 |
+
box-shadow: 0 1px 4px #eee;
|
| 186 |
+
font-size: 15px;
|
| 187 |
+
}
|
| 188 |
+
.el-table th, .el-table td {
|
| 189 |
+
padding-left: 2px;
|
| 190 |
+
padding-right: 2px;
|
| 191 |
+
}
|
| 192 |
+
.el-pagination {
|
| 193 |
+
font-size: 15px;
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
</style>
|
| 197 |
+
</head>
|
| 198 |
+
<body>
|
| 199 |
+
<!--- /> 标签改为标准的成对闭合形式,否则显示将不正常-->
|
| 200 |
+
<div id="app" class="app-container">
|
| 201 |
+
<el-container>
|
| 202 |
+
<el-header style="height: auto; padding: 0; margin-bottom: 20px;">
|
| 203 |
+
<div class="dashboard-card">
|
| 204 |
+
<div class="dashboard-header">
|
| 205 |
+
<h2>JetBrains激活链接管理系统</h2>
|
| 206 |
+
<div>
|
| 207 |
+
<el-button type="primary" @click="showImportDialog">导入数据</el-button>
|
| 208 |
+
<el-button type="success" @click="showExportDialog">导出数据</el-button>
|
| 209 |
+
<el-button type="danger" @click="resetFailedAccounts">重置失败账号</el-button>
|
| 210 |
+
<el-button type="warning" @click="logout" style="margin-left: 10px;">退出登录</el-button>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
</el-header>
|
| 215 |
+
|
| 216 |
+
<el-main>
|
| 217 |
+
<!-- 统计信息卡片 /> 标签改为标准的成对闭合形式,否则显示将不正常 -->
|
| 218 |
+
<el-row :gutter="20">
|
| 219 |
+
<el-col :span="4">
|
| 220 |
+
<el-card shadow="hover" class="statistics-card">
|
| 221 |
+
<div class="statistics-value">{{ statistics.total }}</div>
|
| 222 |
+
<div class="statistics-label">总数</div>
|
| 223 |
+
</el-card>
|
| 224 |
+
</el-col>
|
| 225 |
+
<!-- <el-col :span="5">
|
| 226 |
+
<el-card shadow="hover" class="statistics-card" style="background-color: #f0f9eb;">
|
| 227 |
+
<div class="statistics-value" style="color: #67C23A;">{{ statistics.link_extracted }}</div>
|
| 228 |
+
<div class="statistics-label">链接</div>
|
| 229 |
+
</el-card>
|
| 230 |
+
</el-col> -->
|
| 231 |
+
<el-col :span="5">
|
| 232 |
+
<el-card shadow="hover" class="statistics-card" style="background-color: #f9f9e3;">
|
| 233 |
+
<div class="statistics-value" style="color: #e6a23c;">{{ statistics.unused }}</div>
|
| 234 |
+
<div class="statistics-label">未用</div>
|
| 235 |
+
</el-card>
|
| 236 |
+
</el-col>
|
| 237 |
+
<el-col :span="5">
|
| 238 |
+
<el-card shadow="hover" class="statistics-card" style="background-color: #ecf5ff;">
|
| 239 |
+
<div class="statistics-value" style="color: #409EFF;">{{ statistics.submitted }}</div>
|
| 240 |
+
<div class="statistics-label">提交</div>
|
| 241 |
+
</el-card>
|
| 242 |
+
</el-col>
|
| 243 |
+
<el-col :span="5">
|
| 244 |
+
<el-card shadow="hover" class="statistics-card" style="background-color: #fef0f0;">
|
| 245 |
+
<div class="statistics-value" style="color: #F56C6C;">{{ statistics.failed }}</div>
|
| 246 |
+
<div class="statistics-label">失败</div>
|
| 247 |
+
</el-card>
|
| 248 |
+
</el-col>
|
| 249 |
+
<el-col :span="5">
|
| 250 |
+
<el-card shadow="hover" class="statistics-card" style="background-color: #f4f4f5;">
|
| 251 |
+
<div class="statistics-value" style="color: #909399;">{{ statistics.pending }}</div>
|
| 252 |
+
<div class="statistics-label">未取</div>
|
| 253 |
+
</el-card>
|
| 254 |
+
</el-col>
|
| 255 |
+
</el-row>
|
| 256 |
+
|
| 257 |
+
<!-- 主要标签页 -->
|
| 258 |
+
<el-tabs type="border-card" v-model="activeTab" @tab-click="handleTabClick">
|
| 259 |
+
<!-- 账号管理标签页 -->
|
| 260 |
+
<el-tab-pane label="账号管理" name="accounts">
|
| 261 |
+
<div class="tab-container">
|
| 262 |
+
<div style="margin-bottom: 20px;">
|
| 263 |
+
<el-row :gutter="10" class="filter-row" style="flex-wrap: nowrap; display: flex;">
|
| 264 |
+
<el-col :span="3" class="filter-col" style="min-width: 120px;">
|
| 265 |
+
<el-select v-model="accountsFilter.status" placeholder="选择状态" clearable style="width: 100%;" @change="handleStatusChange">
|
| 266 |
+
<el-option label="未提交" :value="0"></el-option>
|
| 267 |
+
<el-option label="已提交" :value="1"></el-option>
|
| 268 |
+
<el-option label="提交失败" :value="2"></el-option>
|
| 269 |
+
<el-option label="已提取链接" :value="3"></el-option>
|
| 270 |
+
</el-select>
|
| 271 |
+
</el-col>
|
| 272 |
+
<el-col :span="3" class="filter-col" style="min-width: 140px;">
|
| 273 |
+
<el-date-picker v-model="accountsFilter.registerTimeStart" type="date" placeholder="注册起始日期" style="width: 100%;" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable></el-date-picker>
|
| 274 |
+
</el-col>
|
| 275 |
+
<el-col :span="3" class="filter-col" style="min-width: 140px;">
|
| 276 |
+
<el-date-picker v-model="accountsFilter.registerTimeEnd" type="date" placeholder="注册结束日期" style="width: 100%;" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable></el-date-picker>
|
| 277 |
+
</el-col>
|
| 278 |
+
<el-col :span="2" class="filter-col" style="min-width: 90px;">
|
| 279 |
+
<el-input v-model="accountsFilter.idMin" placeholder="最小ID" clearable></el-input>
|
| 280 |
+
</el-col>
|
| 281 |
+
<el-col :span="2" class="filter-col" style="min-width: 90px;">
|
| 282 |
+
<el-input v-model="accountsFilter.idMax" placeholder="最大ID" clearable></el-input>
|
| 283 |
+
</el-col>
|
| 284 |
+
<el-col :span="3" class="filter-col" style="min-width: 120px;">
|
| 285 |
+
<el-select v-model="accountsFilter.hasActivationLink" placeholder="激活链接" clearable style="width: 100%;">
|
| 286 |
+
<el-option label="全部" value=""></el-option>
|
| 287 |
+
<el-option label="有激活链接" value="yes"></el-option>
|
| 288 |
+
<el-option label="无激活链接" value="no"></el-option>
|
| 289 |
+
</el-select>
|
| 290 |
+
</el-col>
|
| 291 |
+
<el-col :span="3" class="filter-col" style="min-width: 120px;">
|
| 292 |
+
<el-select v-model="accountsFilter.used" placeholder="使用状态" clearable style="width: 100%;">
|
| 293 |
+
<el-option label="全部" :value="''"></el-option>
|
| 294 |
+
<el-option label="未被使用" :value="0"></el-option>
|
| 295 |
+
<el-option label="已被使用" :value="1"></el-option>
|
| 296 |
+
</el-select>
|
| 297 |
+
</el-col>
|
| 298 |
+
<el-col :span="4" class="filter-col" style="min-width: 160px;">
|
| 299 |
+
<el-input v-model="accountsFilter.search" placeholder="邮箱/备注" clearable></el-input>
|
| 300 |
+
</el-col>
|
| 301 |
+
</el-row>
|
| 302 |
+
</div>
|
| 303 |
+
|
| 304 |
+
<el-row :gutter="20" style="margin-bottom: 10px;">
|
| 305 |
+
<el-col :span="24" style="text-align: right;">
|
| 306 |
+
<el-button type="primary" :disabled="selectedAccounts.length === 0" @click="batchToggleUsed(1)">
|
| 307 |
+
批量标记为已使用
|
| 308 |
+
</el-button>
|
| 309 |
+
<el-button type="info" :disabled="selectedAccounts.length === 0" @click="batchToggleUsed(0)">
|
| 310 |
+
批量标记为未使用
|
| 311 |
+
</el-button>
|
| 312 |
+
</el-col>
|
| 313 |
+
</el-row>
|
| 314 |
+
|
| 315 |
+
<el-table
|
| 316 |
+
:data="accounts"
|
| 317 |
+
style="width: 100%;overflow-x:auto;"
|
| 318 |
+
v-loading="loading.accounts"
|
| 319 |
+
@selection-change="handleSelectionChange"
|
| 320 |
+
border
|
| 321 |
+
stripe
|
| 322 |
+
highlight-current-row
|
| 323 |
+
>
|
| 324 |
+
<el-table-column type="selection" width="50"> </el-table-column>
|
| 325 |
+
<el-table-column label="ID" width="80" sortable>
|
| 326 |
+
<template #default="scope">
|
| 327 |
+
{{ scope.row.id || '-' }}
|
| 328 |
+
</template>
|
| 329 |
+
</el-table-column>
|
| 330 |
+
<el-table-column label="注册时间" width="180" sortable>
|
| 331 |
+
<template #default="scope">
|
| 332 |
+
{{ scope.row.register_time || '-' }}
|
| 333 |
+
</template>
|
| 334 |
+
</el-table-column>
|
| 335 |
+
<el-table-column label="用户名" min-width="250" show-overflow-tooltip>
|
| 336 |
+
<template #default="scope">
|
| 337 |
+
{{ scope.row.username || '-' }}
|
| 338 |
+
</template>
|
| 339 |
+
</el-table-column>
|
| 340 |
+
<el-table-column label="状态" width="120">
|
| 341 |
+
<template #default="scope">
|
| 342 |
+
<el-tag :type="getStatusType(scope.row.status)" size="small">
|
| 343 |
+
{{ getStatusText(scope.row.status) }}
|
| 344 |
+
</el-tag>
|
| 345 |
+
</template>
|
| 346 |
+
</el-table-column>
|
| 347 |
+
<el-table-column label="激活链接" min-width="300">
|
| 348 |
+
<template #default="scope">
|
| 349 |
+
<div v-if="scope.row.activation_link" style="display: flex; align-items: center;">
|
| 350 |
+
<el-tooltip :content="scope.row.activation_link" placement="top" effect="light">
|
| 351 |
+
<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px;">
|
| 352 |
+
{{ scope.row.activation_link }}
|
| 353 |
+
</span>
|
| 354 |
+
</el-tooltip>
|
| 355 |
+
<el-button
|
| 356 |
+
size="small"
|
| 357 |
+
type="primary"
|
| 358 |
+
class="copy-button"
|
| 359 |
+
@click="copyToClipboard(scope.row.activation_link, scope.row)"
|
| 360 |
+
>
|
| 361 |
+
复制
|
| 362 |
+
</el-button>
|
| 363 |
+
</div>
|
| 364 |
+
<span v-else>-</span>
|
| 365 |
+
</template>
|
| 366 |
+
</el-table-column>
|
| 367 |
+
<el-table-column label="已被使用" width="100">
|
| 368 |
+
<template #default="scope">
|
| 369 |
+
<el-tag :type="scope.row.used ? 'success' : 'info'" @click="toggleAccountUsed(scope.row)" style="cursor:pointer;">
|
| 370 |
+
{{ scope.row.used ? '✔ 已使用' : '未使用' }}
|
| 371 |
+
</el-tag>
|
| 372 |
+
</template>
|
| 373 |
+
</el-table-column>
|
| 374 |
+
<el-table-column label="操作" width="250" fixed="right">
|
| 375 |
+
<template #default="scope">
|
| 376 |
+
<el-button
|
| 377 |
+
size="small"
|
| 378 |
+
type="primary"
|
| 379 |
+
@click="submitSingleAccount(scope.row)"
|
| 380 |
+
:disabled="scope.row.status === 1 || scope.row.status === 3"
|
| 381 |
+
>
|
| 382 |
+
提交
|
| 383 |
+
</el-button>
|
| 384 |
+
<el-button
|
| 385 |
+
size="small"
|
| 386 |
+
type="success"
|
| 387 |
+
@click="extractSingleAccount(scope.row)"
|
| 388 |
+
:disabled="scope.row.status !== 1"
|
| 389 |
+
>
|
| 390 |
+
提取
|
| 391 |
+
</el-button>
|
| 392 |
+
<el-button
|
| 393 |
+
size="small"
|
| 394 |
+
@click="viewAccountDetails(scope.row)"
|
| 395 |
+
>
|
| 396 |
+
详情
|
| 397 |
+
</el-button>
|
| 398 |
+
<el-button
|
| 399 |
+
size="small"
|
| 400 |
+
type="danger"
|
| 401 |
+
@click="deleteAccount(scope.row)"
|
| 402 |
+
>
|
| 403 |
+
删除
|
| 404 |
+
</el-button>
|
| 405 |
+
</template>
|
| 406 |
+
</el-table-column>
|
| 407 |
+
</el-table>
|
| 408 |
+
|
| 409 |
+
<div style="margin-top: 20px; display: flex; justify-content: center;">
|
| 410 |
+
<el-pagination
|
| 411 |
+
v-model:current-page="pagination.current"
|
| 412 |
+
v-model:page-size="pagination.pageSize"
|
| 413 |
+
:page-sizes="[10, 20, 50, 100]"
|
| 414 |
+
layout="total, sizes, prev, pager, next, jumper"
|
| 415 |
+
:total="pagination.total"
|
| 416 |
+
@size-change="handleSizeChange"
|
| 417 |
+
@current-change="handleCurrentChange"
|
| 418 |
+
/>
|
| 419 |
+
</div>
|
| 420 |
+
</div>
|
| 421 |
+
</el-tab-pane>
|
| 422 |
+
|
| 423 |
+
<!-- 任务管理标签页 -->
|
| 424 |
+
<el-tab-pane label="任务管理" name="tasks">
|
| 425 |
+
<div class="tab-container">
|
| 426 |
+
<el-button type="primary" @click="loadTasks" style="margin-bottom: 20px;">刷新任务列表</el-button>
|
| 427 |
+
|
| 428 |
+
<el-timeline>
|
| 429 |
+
<el-timeline-item
|
| 430 |
+
v-for="(task, id) in tasks"
|
| 431 |
+
:key="id"
|
| 432 |
+
:type="getTaskStatusType(task.status)"
|
| 433 |
+
:icon="getTaskStatusIcon(task.status)"
|
| 434 |
+
:timestamp="task.start_time"
|
| 435 |
+
>
|
| 436 |
+
<el-card class="task-card">
|
| 437 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 438 |
+
<div>
|
| 439 |
+
<span style="font-weight: bold;">{{ getTaskTypeText(task.type) }} - {{ id }}</span>
|
| 440 |
+
<el-tag size="small" style="margin-left: 10px;" :type="getTaskStatusType(task.status)">
|
| 441 |
+
{{ getTaskStatusText(task.status) }}
|
| 442 |
+
</el-tag>
|
| 443 |
+
</div>
|
| 444 |
+
<div v-if="task.status === 'completed'">
|
| 445 |
+
<span style="margin-right: 10px;">成功: {{ task.success_count || 0 }}</span>
|
| 446 |
+
<span>失败: {{ task.error_count || 0 }}</span>
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
<div v-if="task.error" style="color: #F56C6C; margin-top: 10px;">
|
| 450 |
+
错误: {{ task.error }}
|
| 451 |
+
</div>
|
| 452 |
+
<div v-if="task.account_ids && task.account_ids.length > 0" style="margin-top: 10px;">
|
| 453 |
+
<span>处理账号: {{ task.account_ids.length }}个</span>
|
| 454 |
+
</div>
|
| 455 |
+
</el-card>
|
| 456 |
+
</el-timeline-item>
|
| 457 |
+
</el-timeline>
|
| 458 |
+
|
| 459 |
+
<div v-if="Object.keys(tasks).length === 0" style="text-align: center; padding: 30px;">
|
| 460 |
+
<el-empty description="暂无任务记录" />
|
| 461 |
+
</div>
|
| 462 |
+
</div>
|
| 463 |
+
</el-tab-pane>
|
| 464 |
+
|
| 465 |
+
<!-- 操作日志标签页 -->
|
| 466 |
+
<el-tab-pane label="操作日志" name="logs">
|
| 467 |
+
<div class="tab-container">
|
| 468 |
+
<div style="margin-bottom: 20px;">
|
| 469 |
+
<el-row :gutter="20">
|
| 470 |
+
<el-col :span="8">
|
| 471 |
+
<el-input v-model="logsFilter.username" placeholder="用户名" clearable></el-input>
|
| 472 |
+
</el-col>
|
| 473 |
+
<el-col :span="8">
|
| 474 |
+
<el-select v-model="logsFilter.operation" placeholder="操作类型" clearable style="width: 100%;">
|
| 475 |
+
<el-option label="提交邮箱" value="submit_email"></el-option>
|
| 476 |
+
<el-option label="提取链接" value="extract_link"></el-option>
|
| 477 |
+
<el-option label="后台提交" value="submit_email_background"></el-option>
|
| 478 |
+
<el-option label="后台提取" value="extract_link_background"></el-option>
|
| 479 |
+
<el-option label="更新状态" value="update_status"></el-option>
|
| 480 |
+
</el-select>
|
| 481 |
+
</el-col>
|
| 482 |
+
<el-col :span="8">
|
| 483 |
+
<el-select v-model="logsFilter.status" placeholder="操作状态" clearable style="width: 100%;">
|
| 484 |
+
<el-option label="成功" value="success"></el-option>
|
| 485 |
+
<el-option label="失败" value="failed"></el-option>
|
| 486 |
+
<el-option label="处理中" value="processing"></el-option>
|
| 487 |
+
<el-option label="错误" value="error"></el-option>
|
| 488 |
+
</el-select>
|
| 489 |
+
</el-col>
|
| 490 |
+
</el-row>
|
| 491 |
+
</div>
|
| 492 |
+
|
| 493 |
+
<el-table
|
| 494 |
+
:data="logs"
|
| 495 |
+
style="width: 100%"
|
| 496 |
+
v-loading="loading.logs"
|
| 497 |
+
border
|
| 498 |
+
>
|
| 499 |
+
<el-table-column label="ID" width="80">
|
| 500 |
+
<template #default="scope">
|
| 501 |
+
{{ scope.row.log_id || scope.row.id || '-' }}
|
| 502 |
+
</template>
|
| 503 |
+
</el-table-column>
|
| 504 |
+
<el-table-column label="用户名" min-width="150">
|
| 505 |
+
<template #default="scope">
|
| 506 |
+
{{ scope.row.account_username || scope.row.username || '-' }}
|
| 507 |
+
</template>
|
| 508 |
+
</el-table-column>
|
| 509 |
+
<el-table-column label="操作" width="180">
|
| 510 |
+
<template #default="scope">
|
| 511 |
+
<el-tag size="small">{{ getOperationText(scope.row.operation) }}</el-tag>
|
| 512 |
+
</template>
|
| 513 |
+
</el-table-column>
|
| 514 |
+
<el-table-column label="状态" width="120">
|
| 515 |
+
<template #default="scope">
|
| 516 |
+
<el-tag :type="getLogStatusType(scope.row.status)" size="small">
|
| 517 |
+
{{ scope.row.status }}
|
| 518 |
+
</el-tag>
|
| 519 |
+
</template>
|
| 520 |
+
</el-table-column>
|
| 521 |
+
<el-table-column label="消息" min-width="200">
|
| 522 |
+
<template #default="scope">
|
| 523 |
+
<el-tag size="small">{{ getMessageText(scope.row.message) }}</el-tag>
|
| 524 |
+
</template>
|
| 525 |
+
</el-table-column>
|
| 526 |
+
<el-table-column label="时间" width="180">
|
| 527 |
+
<template #default="scope">
|
| 528 |
+
{{ scope.row.created_at || scope.row.timestamp || '-' }}
|
| 529 |
+
</template>
|
| 530 |
+
</el-table-column>
|
| 531 |
+
</el-table>
|
| 532 |
+
|
| 533 |
+
<div style="margin-top: 20px; display: flex; justify-content: center;">
|
| 534 |
+
<el-pagination
|
| 535 |
+
v-model:current-page="logsPagination.current"
|
| 536 |
+
v-model:page-size="logsPagination.pageSize"
|
| 537 |
+
:page-sizes="[20, 50, 100]"
|
| 538 |
+
layout="total, sizes, prev, pager, next, jumper"
|
| 539 |
+
:total="logsPagination.total"
|
| 540 |
+
@size-change="handleLogsSizeChange"
|
| 541 |
+
@current-change="handleLogsCurrentChange"
|
| 542 |
+
/>
|
| 543 |
+
</div>
|
| 544 |
+
</div>
|
| 545 |
+
</el-tab-pane>
|
| 546 |
+
</el-tabs>
|
| 547 |
+
</el-main>
|
| 548 |
+
</el-container>
|
| 549 |
+
|
| 550 |
+
<!-- 导入数据对话框 -->
|
| 551 |
+
<el-dialog title="导入数据" v-model="dialogs.import" width="500px">
|
| 552 |
+
<el-form label-width="100px">
|
| 553 |
+
<el-form-item label="选择文件" required>
|
| 554 |
+
<input type="file" @change="handleImportFileChange" accept=".txt,.csv" />
|
| 555 |
+
</el-form-item>
|
| 556 |
+
</el-form>
|
| 557 |
+
<template #footer>
|
| 558 |
+
<span class="dialog-footer">
|
| 559 |
+
<el-button @click="dialogs.import = false">取消</el-button>
|
| 560 |
+
<el-button type="primary" @click="importAccounts" :loading="loading.import">导入</el-button>
|
| 561 |
+
</span>
|
| 562 |
+
</template>
|
| 563 |
+
</el-dialog>
|
| 564 |
+
|
| 565 |
+
<!-- 导出数据对话框 -->
|
| 566 |
+
<el-dialog title="导出数据" v-model="dialogs.export" width="500px">
|
| 567 |
+
<el-form :model="exportForm" label-width="100px">
|
| 568 |
+
<el-form-item label="文件名">
|
| 569 |
+
<el-input v-model="exportForm.filename" placeholder="导出文件名"></el-input>
|
| 570 |
+
</el-form-item>
|
| 571 |
+
</el-form>
|
| 572 |
+
<template #footer>
|
| 573 |
+
<span class="dialog-footer">
|
| 574 |
+
<el-button @click="dialogs.export = false">取消</el-button>
|
| 575 |
+
<el-button type="primary" @click="exportAccounts" :loading="loading.export">导出</el-button>
|
| 576 |
+
</span>
|
| 577 |
+
</template>
|
| 578 |
+
</el-dialog>
|
| 579 |
+
|
| 580 |
+
<!-- 账号详情对话框 -->
|
| 581 |
+
<el-dialog title="账号详情" v-model="dialogs.accountDetails" width="700px">
|
| 582 |
+
<el-descriptions v-if="currentAccount" :column="1" border>
|
| 583 |
+
<el-descriptions-item label="ID">{{ currentAccount.id }}</el-descriptions-item>
|
| 584 |
+
<el-descriptions-item label="注册时间">{{ currentAccount.register_time }}</el-descriptions-item>
|
| 585 |
+
<el-descriptions-item label="用户名">{{ currentAccount.username }}</el-descriptions-item>
|
| 586 |
+
<el-descriptions-item label="密码">{{ currentAccount.password }}</el-descriptions-item>
|
| 587 |
+
<el-descriptions-item label="密保邮箱">{{ currentAccount.security_email || '-' }}</el-descriptions-item>
|
| 588 |
+
<el-descriptions-item label="状态">
|
| 589 |
+
<el-tag :type="getStatusType(currentAccount.status)">
|
| 590 |
+
{{ getStatusText(currentAccount.status) }}
|
| 591 |
+
</el-tag>
|
| 592 |
+
</el-descriptions-item>
|
| 593 |
+
<el-descriptions-item label="激活链接">
|
| 594 |
+
<div v-if="currentAccount.activation_link" style="display: flex; align-items: center;">
|
| 595 |
+
<span style="word-break: break-all;">{{ currentAccount.activation_link }}</span>
|
| 596 |
+
<el-button
|
| 597 |
+
size="small"
|
| 598 |
+
type="primary"
|
| 599 |
+
class="copy-button"
|
| 600 |
+
@click="copyToClipboard(currentAccount.activation_link, currentAccount)"
|
| 601 |
+
>
|
| 602 |
+
复制
|
| 603 |
+
</el-button>
|
| 604 |
+
</div>
|
| 605 |
+
<span v-else>-</span>
|
| 606 |
+
</el-descriptions-item>
|
| 607 |
+
<el-descriptions-item label="更新时间">{{ currentAccount.updated_at || '-' }}</el-descriptions-item>
|
| 608 |
+
<el-descriptions-item label="备注">{{ currentAccount.notes || '-' }}</el-descriptions-item>
|
| 609 |
+
</el-descriptions>
|
| 610 |
+
|
| 611 |
+
<div style="margin-top: 20px;">
|
| 612 |
+
<h3>更新状态</h3>
|
| 613 |
+
<el-form :model="statusUpdateForm" label-width="100px">
|
| 614 |
+
<el-form-item label="状态">
|
| 615 |
+
<el-select v-model="statusUpdateForm.status" placeholder="选择状态" style="width: 100%;">
|
| 616 |
+
<el-option label="未提交" :value="0"></el-option>
|
| 617 |
+
<el-option label="已提交" :value="1"></el-option>
|
| 618 |
+
<el-option label="提交失败" :value="2"></el-option>
|
| 619 |
+
<el-option label="已提取链接" :value="3"></el-option>
|
| 620 |
+
</el-select>
|
| 621 |
+
</el-form-item>
|
| 622 |
+
<el-form-item label="激活链接">
|
| 623 |
+
<el-input v-model="statusUpdateForm.activation_link" placeholder="可选"></el-input>
|
| 624 |
+
</el-form-item>
|
| 625 |
+
<el-form-item label="备注">
|
| 626 |
+
<el-input v-model="statusUpdateForm.notes" type="textarea" placeholder="可选"></el-input>
|
| 627 |
+
</el-form-item>
|
| 628 |
+
</el-form>
|
| 629 |
+
</div>
|
| 630 |
+
|
| 631 |
+
<template #footer>
|
| 632 |
+
<span class="dialog-footer">
|
| 633 |
+
<el-button @click="dialogs.accountDetails = false">取消</el-button>
|
| 634 |
+
<el-button type="primary" @click="updateAccountStatus" :loading="loading.updateStatus">更新</el-button>
|
| 635 |
+
</span>
|
| 636 |
+
</template>
|
| 637 |
+
</el-dialog>
|
| 638 |
+
|
| 639 |
+
<!-- 提交邮箱对话框 -->
|
| 640 |
+
<el-dialog title="提交邮箱" v-model="dialogs.submit" width="500px">
|
| 641 |
+
<el-form :model="submitForm" label-width="100px">
|
| 642 |
+
<el-form-item label="最大线程数">
|
| 643 |
+
<el-input-number v-model="submitForm.max_workers" :min="1" :max="10"></el-input-number>
|
| 644 |
+
</el-form-item>
|
| 645 |
+
<el-form-item label="代理服务器">
|
| 646 |
+
<el-input v-model="submitForm.proxy" placeholder="可选,例如:http://127.0.0.1:60060"></el-input>
|
| 647 |
+
</el-form-item>
|
| 648 |
+
</el-form>
|
| 649 |
+
<template #footer>
|
| 650 |
+
<span class="dialog-footer">
|
| 651 |
+
<el-button @click="dialogs.submit = false">取消</el-button>
|
| 652 |
+
<el-button type="primary" @click="confirmSubmit" :loading="loading.submit">提交</el-button>
|
| 653 |
+
</span>
|
| 654 |
+
</template>
|
| 655 |
+
</el-dialog>
|
| 656 |
+
|
| 657 |
+
<!-- 提取链接对话框 -->
|
| 658 |
+
<el-dialog title="提取链接" v-model="dialogs.extract" width="500px">
|
| 659 |
+
<el-form :model="extractForm" label-width="100px">
|
| 660 |
+
<el-form-item label="最大线程数">
|
| 661 |
+
<el-input-number v-model="extractForm.max_workers" :min="1" :max="5"></el-input-number>
|
| 662 |
+
</el-form-item>
|
| 663 |
+
</el-form>
|
| 664 |
+
<template #footer>
|
| 665 |
+
<span class="dialog-footer">
|
| 666 |
+
<el-button @click="dialogs.extract = false">取消</el-button>
|
| 667 |
+
<el-button type="primary" @click="confirmExtract" :loading="loading.extract">提取</el-button>
|
| 668 |
+
</span>
|
| 669 |
+
</template>
|
| 670 |
+
</el-dialog>
|
| 671 |
+
|
| 672 |
+
<!-- 登录验证对话框 -->
|
| 673 |
+
<el-dialog title="登录验证" v-model="dialogs.login" width="350px" :close-on-click-modal="false" :show-close="false">
|
| 674 |
+
<el-form @submit.prevent="handleLogin">
|
| 675 |
+
<el-form-item label="密码">
|
| 676 |
+
<el-input v-model="loginForm.password" type="password" autocomplete="off" @keyup.enter="handleLogin"></el-input>
|
| 677 |
+
</el-form-item>
|
| 678 |
+
</el-form>
|
| 679 |
+
<template #footer>
|
| 680 |
+
<span class="dialog-footer">
|
| 681 |
+
<el-button type="primary" @click="handleLogin" :loading="loading.login">登录</el-button>
|
| 682 |
+
</span>
|
| 683 |
+
</template>
|
| 684 |
+
</el-dialog>
|
| 685 |
+
</div>
|
| 686 |
+
|
| 687 |
+
<script>
|
| 688 |
+
const { createApp, ref, reactive, onMounted, computed, nextTick, watch, onUnmounted } = Vue;
|
| 689 |
+
|
| 690 |
+
// 后端API基础URL
|
| 691 |
+
const API_BASE_URL = 'http://192.168.2.8:8000';
|
| 692 |
+
|
| 693 |
+
const app = createApp({
|
| 694 |
+
setup() {
|
| 695 |
+
// 状态管理
|
| 696 |
+
const activeTab = ref('accounts');
|
| 697 |
+
const accounts = ref([]);
|
| 698 |
+
const selectedAccounts = ref([]);
|
| 699 |
+
const currentAccount = ref(null);
|
| 700 |
+
const tasks = ref({});
|
| 701 |
+
const logs = ref([]);
|
| 702 |
+
const statistics = reactive({
|
| 703 |
+
total: 0,
|
| 704 |
+
pending: 0,
|
| 705 |
+
submitted: 0,
|
| 706 |
+
failed: 0,
|
| 707 |
+
link_extracted: 0,
|
| 708 |
+
unused: 0
|
| 709 |
+
});
|
| 710 |
+
|
| 711 |
+
// 任务轮询相关状态
|
| 712 |
+
const runningTaskPolls = reactive({});
|
| 713 |
+
|
| 714 |
+
// 分页配置
|
| 715 |
+
const pagination = reactive({
|
| 716 |
+
current: 1,
|
| 717 |
+
pageSize: 10,
|
| 718 |
+
total: 0
|
| 719 |
+
});
|
| 720 |
+
|
| 721 |
+
const logsPagination = reactive({
|
| 722 |
+
current: 1,
|
| 723 |
+
pageSize: 20,
|
| 724 |
+
total: 0
|
| 725 |
+
});
|
| 726 |
+
|
| 727 |
+
// 过滤器
|
| 728 |
+
const accountsFilter = reactive({
|
| 729 |
+
status: null,
|
| 730 |
+
search: '',
|
| 731 |
+
registerTimeStart: '',
|
| 732 |
+
registerTimeEnd: '',
|
| 733 |
+
idMin: '',
|
| 734 |
+
idMax: '',
|
| 735 |
+
hasActivationLink: '', // '', 'yes', 'no'
|
| 736 |
+
used: '' // 新增
|
| 737 |
+
});
|
| 738 |
+
|
| 739 |
+
const logsFilter = reactive({
|
| 740 |
+
username: '',
|
| 741 |
+
operation: '',
|
| 742 |
+
status: ''
|
| 743 |
+
});
|
| 744 |
+
|
| 745 |
+
// 表单数据
|
| 746 |
+
const importForm = reactive({
|
| 747 |
+
filepath: 'user-4-21-success.txt'
|
| 748 |
+
});
|
| 749 |
+
|
| 750 |
+
const exportForm = reactive({
|
| 751 |
+
filename: 'export_results.txt',
|
| 752 |
+
status: null
|
| 753 |
+
});
|
| 754 |
+
|
| 755 |
+
const statusUpdateForm = reactive({
|
| 756 |
+
status: 0,
|
| 757 |
+
activation_link: '',
|
| 758 |
+
notes: ''
|
| 759 |
+
});
|
| 760 |
+
|
| 761 |
+
const submitForm = reactive({
|
| 762 |
+
max_workers: 1,
|
| 763 |
+
proxy: ''
|
| 764 |
+
});
|
| 765 |
+
|
| 766 |
+
const extractForm = reactive({
|
| 767 |
+
max_workers: 1
|
| 768 |
+
});
|
| 769 |
+
|
| 770 |
+
const loginForm = reactive({ password: '' });
|
| 771 |
+
|
| 772 |
+
// 对话框控制
|
| 773 |
+
const dialogs = reactive({
|
| 774 |
+
import: false,
|
| 775 |
+
export: false,
|
| 776 |
+
accountDetails: false,
|
| 777 |
+
submit: false,
|
| 778 |
+
extract: false,
|
| 779 |
+
login: false
|
| 780 |
+
});
|
| 781 |
+
|
| 782 |
+
// 加载状态
|
| 783 |
+
const loading = reactive({
|
| 784 |
+
accounts: false,
|
| 785 |
+
tasks: false,
|
| 786 |
+
logs: false,
|
| 787 |
+
statistics: false,
|
| 788 |
+
import: false,
|
| 789 |
+
export: false,
|
| 790 |
+
updateStatus: false,
|
| 791 |
+
submit: false,
|
| 792 |
+
extract: false,
|
| 793 |
+
login: false
|
| 794 |
+
});
|
| 795 |
+
|
| 796 |
+
// token管理
|
| 797 |
+
const getToken = () => {
|
| 798 |
+
return document.cookie.replace(/(?:(?:^|.*;\s*)token\s*\=\s*([^;]*).*$)|^.*$/, "$1");
|
| 799 |
+
};
|
| 800 |
+
const setToken = (token) => {
|
| 801 |
+
document.cookie = `token=${token};path=/;max-age=2592000`;
|
| 802 |
+
};
|
| 803 |
+
const clearToken = () => {
|
| 804 |
+
document.cookie = 'token=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
| 805 |
+
};
|
| 806 |
+
|
| 807 |
+
// axios请求拦截
|
| 808 |
+
axios.interceptors.request.use(config => {
|
| 809 |
+
const token = getToken();
|
| 810 |
+
if (token) config.headers.token = token;
|
| 811 |
+
return config;
|
| 812 |
+
});
|
| 813 |
+
|
| 814 |
+
// 初始化
|
| 815 |
+
onMounted(async () => {
|
| 816 |
+
if (!(await checkLogin())) return;
|
| 817 |
+
console.log('应用已加载,开始获取数据');
|
| 818 |
+
loadAccounts();
|
| 819 |
+
loadStatistics();
|
| 820 |
+
loadTasks();
|
| 821 |
+
|
| 822 |
+
// 监听过滤器变化
|
| 823 |
+
watch([
|
| 824 |
+
() => accountsFilter.status,
|
| 825 |
+
() => accountsFilter.search,
|
| 826 |
+
() => accountsFilter.registerTimeStart,
|
| 827 |
+
() => accountsFilter.registerTimeEnd,
|
| 828 |
+
() => accountsFilter.idMin,
|
| 829 |
+
() => accountsFilter.idMax,
|
| 830 |
+
() => accountsFilter.hasActivationLink,
|
| 831 |
+
() => accountsFilter.used
|
| 832 |
+
], () => {
|
| 833 |
+
pagination.current = 1;
|
| 834 |
+
loadAccounts();
|
| 835 |
+
});
|
| 836 |
+
|
| 837 |
+
watch([
|
| 838 |
+
() => logsFilter.username,
|
| 839 |
+
() => logsFilter.operation,
|
| 840 |
+
() => logsFilter.status
|
| 841 |
+
], () => {
|
| 842 |
+
logsPagination.current = 1;
|
| 843 |
+
loadLogs();
|
| 844 |
+
});
|
| 845 |
+
});
|
| 846 |
+
|
| 847 |
+
// 登录逻辑
|
| 848 |
+
const handleLogin = async () => {
|
| 849 |
+
if (!loginForm.password) {
|
| 850 |
+
ElementPlus.ElMessage.warning('请输入密码');
|
| 851 |
+
return;
|
| 852 |
+
}
|
| 853 |
+
loading.login = true;
|
| 854 |
+
try {
|
| 855 |
+
const res = await axios.post(`${API_BASE_URL}/login`, { password: loginForm.password });
|
| 856 |
+
setToken(res.data.token);
|
| 857 |
+
dialogs.login = false;
|
| 858 |
+
ElementPlus.ElMessage.success('登录成功');
|
| 859 |
+
// 登录后自动加载数据
|
| 860 |
+
loadAccounts();
|
| 861 |
+
loadStatistics();
|
| 862 |
+
loadTasks();
|
| 863 |
+
} catch (e) {
|
| 864 |
+
ElementPlus.ElMessage.error(e.response?.data?.detail || '登录失败');
|
| 865 |
+
} finally {
|
| 866 |
+
loading.login = false;
|
| 867 |
+
}
|
| 868 |
+
};
|
| 869 |
+
|
| 870 |
+
// 退出登录
|
| 871 |
+
const logout = () => {
|
| 872 |
+
clearToken();
|
| 873 |
+
dialogs.login = true;
|
| 874 |
+
};
|
| 875 |
+
|
| 876 |
+
// 检查token
|
| 877 |
+
const checkLogin = async () => {
|
| 878 |
+
const token = getToken();
|
| 879 |
+
if (!token) {
|
| 880 |
+
dialogs.login = true;
|
| 881 |
+
return false;
|
| 882 |
+
}
|
| 883 |
+
// 简单校验token有效性(可选:请求一个接口)
|
| 884 |
+
try {
|
| 885 |
+
await axios.get(`${API_BASE_URL}/statistics`);
|
| 886 |
+
return true;
|
| 887 |
+
} catch {
|
| 888 |
+
dialogs.login = true;
|
| 889 |
+
return false;
|
| 890 |
+
}
|
| 891 |
+
};
|
| 892 |
+
|
| 893 |
+
// 轮询任务状态的功能
|
| 894 |
+
const startTaskPolling = (taskId) => {
|
| 895 |
+
console.log(`开始轮询任务 ${taskId} 的状态`);
|
| 896 |
+
|
| 897 |
+
if (runningTaskPolls[taskId]) {
|
| 898 |
+
// 如果已经在轮询这个任务,则不重复创建
|
| 899 |
+
return;
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
// 创建一个轮询间隔,每10秒查询一次任务状态
|
| 903 |
+
const intervalId = setInterval(async () => {
|
| 904 |
+
try {
|
| 905 |
+
const response = await axios.get(`${API_BASE_URL}/tasks/${taskId}`);
|
| 906 |
+
const taskData = response.data;
|
| 907 |
+
|
| 908 |
+
// 更新本地任务数据
|
| 909 |
+
tasks.value = { ...tasks.value, [taskId]: taskData };
|
| 910 |
+
|
| 911 |
+
// 如果任务已完成或失败,停止轮询
|
| 912 |
+
if (taskData.status === 'completed' || taskData.status === 'failed') {
|
| 913 |
+
console.log(`任务 ${taskId} 已${taskData.status === 'completed' ? '完成' : '失败'},停止轮询`);
|
| 914 |
+
clearInterval(runningTaskPolls[taskId]);
|
| 915 |
+
delete runningTaskPolls[taskId];
|
| 916 |
+
|
| 917 |
+
// 提醒用户任务已完成
|
| 918 |
+
ElementPlus.ElMessage({
|
| 919 |
+
message: `任务 ${taskId} 已${taskData.status === 'completed' ? '完成' : '失败'},成功: ${taskData.success_count || 0},失败: ${taskData.error_count || 0}`,
|
| 920 |
+
type: taskData.status === 'completed' ? 'success' : 'warning',
|
| 921 |
+
duration: 5000
|
| 922 |
+
});
|
| 923 |
+
|
| 924 |
+
// 如果任务成功,刷新账号列表和统计信息
|
| 925 |
+
if (taskData.status === 'completed') {
|
| 926 |
+
loadAccounts();
|
| 927 |
+
loadStatistics();
|
| 928 |
+
}
|
| 929 |
+
}
|
| 930 |
+
} catch (error) {
|
| 931 |
+
console.error(`轮询任务 ${taskId} 状态失败:`, error);
|
| 932 |
+
// 如果出错,也停止轮询
|
| 933 |
+
clearInterval(runningTaskPolls[taskId]);
|
| 934 |
+
delete runningTaskPolls[taskId];
|
| 935 |
+
}
|
| 936 |
+
}, 10000); // 每10秒轮询一次
|
| 937 |
+
|
| 938 |
+
// 保存轮询间隔ID,以便稍后可以停止它
|
| 939 |
+
runningTaskPolls[taskId] = intervalId;
|
| 940 |
+
};
|
| 941 |
+
|
| 942 |
+
// 加载账号列表
|
| 943 |
+
const loadAccounts = async () => {
|
| 944 |
+
loading.accounts = true;
|
| 945 |
+
try {
|
| 946 |
+
const params = {
|
| 947 |
+
page: pagination.current,
|
| 948 |
+
per_page: pagination.pageSize
|
| 949 |
+
};
|
| 950 |
+
|
| 951 |
+
if (accountsFilter.status !== null && accountsFilter.status !== '') params.status = accountsFilter.status;
|
| 952 |
+
if (accountsFilter.search) params.search = accountsFilter.search;
|
| 953 |
+
if (accountsFilter.registerTimeStart) params.register_time_start = accountsFilter.registerTimeStart;
|
| 954 |
+
if (accountsFilter.registerTimeEnd) params.register_time_end = accountsFilter.registerTimeEnd;
|
| 955 |
+
if (accountsFilter.idMin) params.id_min = accountsFilter.idMin;
|
| 956 |
+
if (accountsFilter.idMax) params.id_max = accountsFilter.idMax;
|
| 957 |
+
if (accountsFilter.hasActivationLink) params.has_activation_link = accountsFilter.hasActivationLink;
|
| 958 |
+
if (accountsFilter.used !== '' && accountsFilter.used !== null && accountsFilter.used !== undefined) params.used = accountsFilter.used;
|
| 959 |
+
|
| 960 |
+
console.log('请求参数:', params);
|
| 961 |
+
const response = await axios.get(`${API_BASE_URL}/accounts`, { params });
|
| 962 |
+
console.log('API响应:', response.data);
|
| 963 |
+
|
| 964 |
+
if (response.data && Array.isArray(response.data.accounts)) {
|
| 965 |
+
accounts.value = response.data.accounts;
|
| 966 |
+
pagination.total = response.data.total;
|
| 967 |
+
|
| 968 |
+
// 调试信息 - 检查每个账号数据
|
| 969 |
+
accounts.value.forEach((account, index) => {
|
| 970 |
+
console.log(`账号[${index}]:`, account);
|
| 971 |
+
if (!account.id) console.warn(`账号[${index}] 缺少ID字段`);
|
| 972 |
+
if (!account.register_time) console.warn(`账号[${index}] 缺少注册时间字段`);
|
| 973 |
+
if (!account.username) console.warn(`账号[${index}] 缺少用户名字段`);
|
| 974 |
+
});
|
| 975 |
+
|
| 976 |
+
// 如果应该有数据但显示不出来,尝试强制更新视图
|
| 977 |
+
if (accounts.value.length > 0) {
|
| 978 |
+
nextTick(() => {
|
| 979 |
+
console.log('视图已更新');
|
| 980 |
+
});
|
| 981 |
+
}
|
| 982 |
+
} else {
|
| 983 |
+
console.error('API返回的数据格式不正确:', response.data);
|
| 984 |
+
ElementPlus.ElMessage.warning('API返回的数据格式不正确');
|
| 985 |
+
}
|
| 986 |
+
} catch (error) {
|
| 987 |
+
console.error('加载账号失败:', error);
|
| 988 |
+
ElementPlus.ElMessage.error('加载账号失败: ' + (error.response?.data?.detail || error.message));
|
| 989 |
+
} finally {
|
| 990 |
+
loading.accounts = false;
|
| 991 |
+
}
|
| 992 |
+
};
|
| 993 |
+
|
| 994 |
+
// 加载统计信息
|
| 995 |
+
const loadStatistics = async () => {
|
| 996 |
+
loading.statistics = true;
|
| 997 |
+
try {
|
| 998 |
+
const response = await axios.get(`${API_BASE_URL}/statistics`);
|
| 999 |
+
Object.assign(statistics, response.data);
|
| 1000 |
+
} catch (error) {
|
| 1001 |
+
console.error('加载统计信息失败:', error);
|
| 1002 |
+
ElementPlus.ElMessage.error('加载统计信息失败: ' + (error.response?.data?.detail || error.message));
|
| 1003 |
+
} finally {
|
| 1004 |
+
loading.statistics = false;
|
| 1005 |
+
}
|
| 1006 |
+
};
|
| 1007 |
+
|
| 1008 |
+
// 加载任务列表
|
| 1009 |
+
const loadTasks = async () => {
|
| 1010 |
+
loading.tasks = true;
|
| 1011 |
+
try {
|
| 1012 |
+
const response = await axios.get(`${API_BASE_URL}/tasks`);
|
| 1013 |
+
tasks.value = response.data;
|
| 1014 |
+
|
| 1015 |
+
// 检查并为所有正在运行的任务启动轮询
|
| 1016 |
+
Object.entries(response.data).forEach(([taskId, taskData]) => {
|
| 1017 |
+
if (taskData.status === 'running' && !runningTaskPolls[taskId]) {
|
| 1018 |
+
startTaskPolling(taskId);
|
| 1019 |
+
}
|
| 1020 |
+
});
|
| 1021 |
+
} catch (error) {
|
| 1022 |
+
console.error('加载任务列表失败:', error);
|
| 1023 |
+
ElementPlus.ElMessage.error('加载任务列表失败: ' + (error.response?.data?.detail || error.message));
|
| 1024 |
+
} finally {
|
| 1025 |
+
loading.tasks = false;
|
| 1026 |
+
}
|
| 1027 |
+
};
|
| 1028 |
+
|
| 1029 |
+
// 加载日志列表
|
| 1030 |
+
const loadLogs = async () => {
|
| 1031 |
+
loading.logs = true;
|
| 1032 |
+
try {
|
| 1033 |
+
const params = {
|
| 1034 |
+
page: logsPagination.current,
|
| 1035 |
+
per_page: logsPagination.pageSize
|
| 1036 |
+
};
|
| 1037 |
+
|
| 1038 |
+
if (logsFilter.username) {
|
| 1039 |
+
params.username = logsFilter.username;
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
if (logsFilter.operation) {
|
| 1043 |
+
params.operation = logsFilter.operation;
|
| 1044 |
+
}
|
| 1045 |
+
|
| 1046 |
+
if (logsFilter.status) {
|
| 1047 |
+
params.status = logsFilter.status;
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
const response = await axios.get(`${API_BASE_URL}/logs`, { params });
|
| 1051 |
+
logs.value = response.data.logs;
|
| 1052 |
+
logsPagination.total = response.data.total;
|
| 1053 |
+
} catch (error) {
|
| 1054 |
+
console.error('加载日志失败:', error);
|
| 1055 |
+
ElementPlus.ElMessage.error('加载日志失败: ' + (error.response?.data?.detail || error.message));
|
| 1056 |
+
} finally {
|
| 1057 |
+
loading.logs = false;
|
| 1058 |
+
}
|
| 1059 |
+
};
|
| 1060 |
+
|
| 1061 |
+
// 分页事件处理
|
| 1062 |
+
const handleSizeChange = (size) => {
|
| 1063 |
+
pagination.pageSize = size;
|
| 1064 |
+
loadAccounts();
|
| 1065 |
+
};
|
| 1066 |
+
|
| 1067 |
+
const handleCurrentChange = (page) => {
|
| 1068 |
+
pagination.current = page;
|
| 1069 |
+
loadAccounts();
|
| 1070 |
+
};
|
| 1071 |
+
|
| 1072 |
+
const handleLogsSizeChange = (size) => {
|
| 1073 |
+
logsPagination.pageSize = size;
|
| 1074 |
+
loadLogs();
|
| 1075 |
+
};
|
| 1076 |
+
|
| 1077 |
+
const handleLogsCurrentChange = (page) => {
|
| 1078 |
+
logsPagination.current = page;
|
| 1079 |
+
loadLogs();
|
| 1080 |
+
};
|
| 1081 |
+
|
| 1082 |
+
// 表格选择事件
|
| 1083 |
+
const handleSelectionChange = (selection) => {
|
| 1084 |
+
selectedAccounts.value = selection;
|
| 1085 |
+
};
|
| 1086 |
+
|
| 1087 |
+
// 标签页点击事件
|
| 1088 |
+
const handleTabClick = (tab) => {
|
| 1089 |
+
if (tab.props.name === 'logs') {
|
| 1090 |
+
loadLogs();
|
| 1091 |
+
}
|
| 1092 |
+
};
|
| 1093 |
+
|
| 1094 |
+
// 导入账号
|
| 1095 |
+
const showImportDialog = () => {
|
| 1096 |
+
dialogs.import = true;
|
| 1097 |
+
};
|
| 1098 |
+
|
| 1099 |
+
const handleImportFileChange = (event) => {
|
| 1100 |
+
const file = event.target.files[0];
|
| 1101 |
+
if (file) {
|
| 1102 |
+
importForm.filepath = file;
|
| 1103 |
+
}
|
| 1104 |
+
};
|
| 1105 |
+
|
| 1106 |
+
const importAccounts = async () => {
|
| 1107 |
+
if (!importForm.filepath) {
|
| 1108 |
+
ElementPlus.ElMessage.warning('请选择文件');
|
| 1109 |
+
return;
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
loading.import = true;
|
| 1113 |
+
try {
|
| 1114 |
+
const formData = new FormData();
|
| 1115 |
+
formData.append('file', importForm.filepath);
|
| 1116 |
+
|
| 1117 |
+
const response = await axios.post(`${API_BASE_URL}/import`, formData, {
|
| 1118 |
+
headers: {
|
| 1119 |
+
'Content-Type': 'multipart/form-data'
|
| 1120 |
+
}
|
| 1121 |
+
});
|
| 1122 |
+
|
| 1123 |
+
ElementPlus.ElMessage.success(`成功导入 ${response.data.imported_count} 个账号`);
|
| 1124 |
+
dialogs.import = false;
|
| 1125 |
+
|
| 1126 |
+
// 重新加载数据
|
| 1127 |
+
loadAccounts();
|
| 1128 |
+
loadStatistics();
|
| 1129 |
+
} catch (error) {
|
| 1130 |
+
console.error('导入账号失败:', error);
|
| 1131 |
+
ElementPlus.ElMessage.error('导入账号失败: ' + (error.response?.data?.detail || error.message));
|
| 1132 |
+
} finally {
|
| 1133 |
+
loading.import = false;
|
| 1134 |
+
}
|
| 1135 |
+
};
|
| 1136 |
+
|
| 1137 |
+
// 导出账号
|
| 1138 |
+
const showExportDialog = () => {
|
| 1139 |
+
dialogs.export = true;
|
| 1140 |
+
};
|
| 1141 |
+
|
| 1142 |
+
const exportAccounts = async () => {
|
| 1143 |
+
if (!exportForm.filename) {
|
| 1144 |
+
ElementPlus.ElMessage.warning('请输入文件名');
|
| 1145 |
+
return;
|
| 1146 |
+
}
|
| 1147 |
+
loading.export = true;
|
| 1148 |
+
try {
|
| 1149 |
+
// 导出时使用当前筛选条件
|
| 1150 |
+
const params = {
|
| 1151 |
+
filename: exportForm.filename
|
| 1152 |
+
};
|
| 1153 |
+
if (accountsFilter.status !== null && accountsFilter.status !== '') params.status = accountsFilter.status;
|
| 1154 |
+
if (accountsFilter.used !== '' && accountsFilter.used !== null && accountsFilter.used !== undefined) params.used = accountsFilter.used;
|
| 1155 |
+
if (accountsFilter.hasActivationLink) params.has_activation_link = accountsFilter.hasActivationLink;
|
| 1156 |
+
if (accountsFilter.search) params.search = accountsFilter.search;
|
| 1157 |
+
if (accountsFilter.registerTimeStart) params.register_time_start = accountsFilter.registerTimeStart;
|
| 1158 |
+
if (accountsFilter.registerTimeEnd) params.register_time_end = accountsFilter.registerTimeEnd;
|
| 1159 |
+
if (accountsFilter.idMin) params.id_min = accountsFilter.idMin;
|
| 1160 |
+
if (accountsFilter.idMax) params.id_max = accountsFilter.idMax;
|
| 1161 |
+
|
| 1162 |
+
// 使用axios以blob方式请求导出数据
|
| 1163 |
+
const response = await axios.post(`${API_BASE_URL}/export`, params, {
|
| 1164 |
+
responseType: 'blob' // 设置响应类型为blob
|
| 1165 |
+
});
|
| 1166 |
+
|
| 1167 |
+
// 创建Blob链接并触发下载
|
| 1168 |
+
const blob = new Blob([response.data], { type: 'text/plain' });
|
| 1169 |
+
const url = window.URL.createObjectURL(blob);
|
| 1170 |
+
const link = document.createElement('a');
|
| 1171 |
+
link.href = url;
|
| 1172 |
+
link.setAttribute('download', exportForm.filename); // 使用用户指定的文件名
|
| 1173 |
+
document.body.appendChild(link);
|
| 1174 |
+
link.click();
|
| 1175 |
+
|
| 1176 |
+
// 清理资源
|
| 1177 |
+
document.body.removeChild(link);
|
| 1178 |
+
window.URL.revokeObjectURL(url);
|
| 1179 |
+
|
| 1180 |
+
ElementPlus.ElMessage.success('文件已导出并下载到本地');
|
| 1181 |
+
dialogs.export = false;
|
| 1182 |
+
} catch (error) {
|
| 1183 |
+
console.error('导出账号失败:', error);
|
| 1184 |
+
ElementPlus.ElMessage.error('导出账号失败: ' + (error.response?.data?.detail || error.message));
|
| 1185 |
+
} finally {
|
| 1186 |
+
loading.export = false;
|
| 1187 |
+
}
|
| 1188 |
+
};
|
| 1189 |
+
|
| 1190 |
+
// 重置失败账号
|
| 1191 |
+
const resetFailedAccounts = async () => {
|
| 1192 |
+
try {
|
| 1193 |
+
const response = await axios.post(`${API_BASE_URL}/reset-failed`);
|
| 1194 |
+
ElementPlus.ElMessage.success(response.data.message);
|
| 1195 |
+
|
| 1196 |
+
// 重新加载数据
|
| 1197 |
+
loadAccounts();
|
| 1198 |
+
loadStatistics();
|
| 1199 |
+
} catch (error) {
|
| 1200 |
+
console.error('重置失败账号出错:', error);
|
| 1201 |
+
ElementPlus.ElMessage.error('重置失败账号出错: ' + (error.response?.data?.detail || error.message));
|
| 1202 |
+
}
|
| 1203 |
+
};
|
| 1204 |
+
|
| 1205 |
+
// 查看账号详情
|
| 1206 |
+
const viewAccountDetails = (account) => {
|
| 1207 |
+
currentAccount.value = {...account};
|
| 1208 |
+
statusUpdateForm.status = account.status;
|
| 1209 |
+
statusUpdateForm.activation_link = account.activation_link || '';
|
| 1210 |
+
statusUpdateForm.notes = account.notes || '';
|
| 1211 |
+
dialogs.accountDetails = true;
|
| 1212 |
+
};
|
| 1213 |
+
|
| 1214 |
+
// 更新账号状态
|
| 1215 |
+
const updateAccountStatus = async () => {
|
| 1216 |
+
if (currentAccount.value === null) {
|
| 1217 |
+
return;
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
loading.updateStatus = true;
|
| 1221 |
+
try {
|
| 1222 |
+
const response = await axios.post(
|
| 1223 |
+
`${API_BASE_URL}/accounts/${currentAccount.value.id}/status`,
|
| 1224 |
+
statusUpdateForm
|
| 1225 |
+
);
|
| 1226 |
+
|
| 1227 |
+
ElementPlus.ElMessage.success('账号状态更新成功');
|
| 1228 |
+
|
| 1229 |
+
// 更新本地数据
|
| 1230 |
+
const index = accounts.value.findIndex(a => a.id === currentAccount.value.id);
|
| 1231 |
+
if (index !== -1) {
|
| 1232 |
+
accounts.value[index] = response.data;
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
dialogs.accountDetails = false;
|
| 1236 |
+
|
| 1237 |
+
// 重新加载统计数据
|
| 1238 |
+
loadStatistics();
|
| 1239 |
+
} catch (error) {
|
| 1240 |
+
console.error('更新账号状态失败:', error);
|
| 1241 |
+
ElementPlus.ElMessage.error('更新账号状态失败: ' + (error.response?.data?.detail || error.message));
|
| 1242 |
+
} finally {
|
| 1243 |
+
loading.updateStatus = false;
|
| 1244 |
+
}
|
| 1245 |
+
};
|
| 1246 |
+
|
| 1247 |
+
// 删除账号
|
| 1248 |
+
const deleteAccount = (account) => {
|
| 1249 |
+
ElementPlus.ElMessageBox.confirm(
|
| 1250 |
+
`确定要删除账号【${account.username}】吗?`,
|
| 1251 |
+
'删除确认',
|
| 1252 |
+
{
|
| 1253 |
+
confirmButtonText: '删除',
|
| 1254 |
+
cancelButtonText: '取消',
|
| 1255 |
+
type: 'warning',
|
| 1256 |
+
}
|
| 1257 |
+
).then(async () => {
|
| 1258 |
+
try {
|
| 1259 |
+
await axios.delete(`${API_BASE_URL}/accounts/${account.id}`);
|
| 1260 |
+
ElementPlus.ElMessage.success('账号已删除');
|
| 1261 |
+
loadAccounts();
|
| 1262 |
+
loadStatistics();
|
| 1263 |
+
} catch (error) {
|
| 1264 |
+
ElementPlus.ElMessage.error('删除失败: ' + (error.response?.data?.detail || error.message));
|
| 1265 |
+
}
|
| 1266 |
+
}).catch(() => {});
|
| 1267 |
+
};
|
| 1268 |
+
|
| 1269 |
+
// 提交邮箱相关
|
| 1270 |
+
const submitSelectedAccounts = () => {
|
| 1271 |
+
submitForm.account_ids = selectedAccounts.value.map(account => account.id);
|
| 1272 |
+
dialogs.submit = true;
|
| 1273 |
+
};
|
| 1274 |
+
|
| 1275 |
+
const submitSingleAccount = (account) => {
|
| 1276 |
+
submitForm.account_ids = [account.id];
|
| 1277 |
+
dialogs.submit = true;
|
| 1278 |
+
};
|
| 1279 |
+
|
| 1280 |
+
const confirmSubmit = async () => {
|
| 1281 |
+
loading.submit = true;
|
| 1282 |
+
try {
|
| 1283 |
+
const response = await axios.post(`${API_BASE_URL}/submit`, {
|
| 1284 |
+
account_ids: submitForm.account_ids,
|
| 1285 |
+
max_workers: submitForm.max_workers,
|
| 1286 |
+
proxy: submitForm.proxy || null
|
| 1287 |
+
});
|
| 1288 |
+
|
| 1289 |
+
const taskId = response.data.task_id;
|
| 1290 |
+
ElementPlus.ElMessage.success('提交任务已启动,任务ID: ' + taskId);
|
| 1291 |
+
dialogs.submit = false;
|
| 1292 |
+
|
| 1293 |
+
// 切换到任务标签页
|
| 1294 |
+
activeTab.value = 'tasks';
|
| 1295 |
+
|
| 1296 |
+
// 延迟刷新任务列表
|
| 1297 |
+
setTimeout(() => {
|
| 1298 |
+
loadTasks();
|
| 1299 |
+
|
| 1300 |
+
// 开始轮询任务状态
|
| 1301 |
+
startTaskPolling(taskId);
|
| 1302 |
+
}, 1000);
|
| 1303 |
+
} catch (error) {
|
| 1304 |
+
console.error('提交邮箱失败:', error);
|
| 1305 |
+
ElementPlus.ElMessage.error('提交邮箱失败: ' + (error.response?.data?.detail || error.message));
|
| 1306 |
+
} finally {
|
| 1307 |
+
loading.submit = false;
|
| 1308 |
+
}
|
| 1309 |
+
};
|
| 1310 |
+
|
| 1311 |
+
// 提取链接相关
|
| 1312 |
+
const extractSelectedAccounts = () => {
|
| 1313 |
+
extractForm.account_ids = selectedAccounts.value
|
| 1314 |
+
.filter(account => account.status === 1)
|
| 1315 |
+
.map(account => account.id);
|
| 1316 |
+
|
| 1317 |
+
if (extractForm.account_ids.length === 0) {
|
| 1318 |
+
ElementPlus.ElMessage.warning('没有选择已提交的账号');
|
| 1319 |
+
return;
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
dialogs.extract = true;
|
| 1323 |
+
};
|
| 1324 |
+
|
| 1325 |
+
const extractSingleAccount = (account) => {
|
| 1326 |
+
if (account.status !== 1) {
|
| 1327 |
+
ElementPlus.ElMessage.warning('只能为已提交的账号提取链接');
|
| 1328 |
+
return;
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
extractForm.account_ids = [account.id];
|
| 1332 |
+
dialogs.extract = true;
|
| 1333 |
+
};
|
| 1334 |
+
|
| 1335 |
+
const confirmExtract = async () => {
|
| 1336 |
+
loading.extract = true;
|
| 1337 |
+
try {
|
| 1338 |
+
const response = await axios.post(`${API_BASE_URL}/extract`, {
|
| 1339 |
+
account_ids: extractForm.account_ids,
|
| 1340 |
+
max_workers: extractForm.max_workers
|
| 1341 |
+
});
|
| 1342 |
+
|
| 1343 |
+
const taskId = response.data.task_id;
|
| 1344 |
+
ElementPlus.ElMessage.success('提取任务已启动,任务ID: ' + taskId);
|
| 1345 |
+
dialogs.extract = false;
|
| 1346 |
+
|
| 1347 |
+
// 切换到任务标签页
|
| 1348 |
+
activeTab.value = 'tasks';
|
| 1349 |
+
|
| 1350 |
+
// 延迟刷新任务列表
|
| 1351 |
+
setTimeout(() => {
|
| 1352 |
+
loadTasks();
|
| 1353 |
+
|
| 1354 |
+
// 开始轮询任务状态
|
| 1355 |
+
startTaskPolling(taskId);
|
| 1356 |
+
}, 1000);
|
| 1357 |
+
} catch (error) {
|
| 1358 |
+
console.error('提取链接失败:', error);
|
| 1359 |
+
ElementPlus.ElMessage.error('提取链接失败: ' + (error.response?.data?.detail || error.message));
|
| 1360 |
+
} finally {
|
| 1361 |
+
loading.extract = false;
|
| 1362 |
+
}
|
| 1363 |
+
};
|
| 1364 |
+
|
| 1365 |
+
// 切换账号used状态
|
| 1366 |
+
const toggleAccountUsed = async (account) => {
|
| 1367 |
+
try {
|
| 1368 |
+
const response = await axios.post(`${API_BASE_URL}/accounts/${account.id}/toggle-used`);
|
| 1369 |
+
account.used = response.data.used;
|
| 1370 |
+
ElementPlus.ElMessage.success('已切换使用状态');
|
| 1371 |
+
} catch (error) {
|
| 1372 |
+
ElementPlus.ElMessage.error('切换失败: ' + (error.response?.data?.detail || error.message));
|
| 1373 |
+
}
|
| 1374 |
+
};
|
| 1375 |
+
|
| 1376 |
+
// 批量切换账号used状态
|
| 1377 |
+
const batchToggleUsed = async (usedValue) => {
|
| 1378 |
+
if (selectedAccounts.value.length === 0) return;
|
| 1379 |
+
const ids = selectedAccounts.value.map(acc => acc.id);
|
| 1380 |
+
const confirmMsg = usedValue ? `确定将选中账号批量标记为“已使用”?` : `确定将选中账号批量标记为“未使用”?`;
|
| 1381 |
+
ElementPlus.ElMessageBox.confirm(confirmMsg, '批量操作确认', {
|
| 1382 |
+
confirmButtonText: '确定',
|
| 1383 |
+
cancelButtonText: '取消',
|
| 1384 |
+
type: 'warning',
|
| 1385 |
+
}).then(async () => {
|
| 1386 |
+
try {
|
| 1387 |
+
await axios.post(`${API_BASE_URL}/accounts/batch-toggle-used`, { ids, used: usedValue });
|
| 1388 |
+
ElementPlus.ElMessage.success('批量操作成功');
|
| 1389 |
+
loadAccounts();
|
| 1390 |
+
} catch (error) {
|
| 1391 |
+
ElementPlus.ElMessage.error('批量操作失败: ' + (error.response?.data?.detail || error.message));
|
| 1392 |
+
}
|
| 1393 |
+
}).catch(() => {});
|
| 1394 |
+
};
|
| 1395 |
+
|
| 1396 |
+
// 复制到剪贴板并自动标记为已使用
|
| 1397 |
+
const copyToClipboard = (text, account) => {
|
| 1398 |
+
navigator.clipboard.writeText(text).then(async () => {
|
| 1399 |
+
ElementPlus.ElMessage({
|
| 1400 |
+
message: '已复制到剪贴板',
|
| 1401 |
+
type: 'success',
|
| 1402 |
+
duration: 1500
|
| 1403 |
+
});
|
| 1404 |
+
// 自动标记为已使用
|
| 1405 |
+
if (account && !account.used) {
|
| 1406 |
+
try {
|
| 1407 |
+
const response = await axios.post(`${API_BASE_URL}/accounts/${account.id}/toggle-used`);
|
| 1408 |
+
account.used = response.data.used;
|
| 1409 |
+
} catch (error) {
|
| 1410 |
+
// 忽略错误
|
| 1411 |
+
}
|
| 1412 |
+
}
|
| 1413 |
+
}).catch(err => {
|
| 1414 |
+
console.error('复制失败:', err);
|
| 1415 |
+
ElementPlus.ElMessage.error('复制失败');
|
| 1416 |
+
});
|
| 1417 |
+
};
|
| 1418 |
+
|
| 1419 |
+
// 清理函数 - 组件卸载时调用
|
| 1420 |
+
onUnmounted(() => {
|
| 1421 |
+
// 清理所有正在运行的轮询
|
| 1422 |
+
Object.values(runningTaskPolls).forEach(intervalId => {
|
| 1423 |
+
clearInterval(intervalId);
|
| 1424 |
+
});
|
| 1425 |
+
});
|
| 1426 |
+
|
| 1427 |
+
// 工具函数 - 获取状态文本
|
| 1428 |
+
const getStatusText = (status) => {
|
| 1429 |
+
const statusMap = {
|
| 1430 |
+
0: '未提交',
|
| 1431 |
+
1: '已提交',
|
| 1432 |
+
2: '提交失败',
|
| 1433 |
+
3: '已提取链接'
|
| 1434 |
+
};
|
| 1435 |
+
return statusMap[status] || '未知状态';
|
| 1436 |
+
};
|
| 1437 |
+
|
| 1438 |
+
// 工具函数 - 获取状态类型(Element UI的tag类型)
|
| 1439 |
+
const getStatusType = (status) => {
|
| 1440 |
+
const typeMap = {
|
| 1441 |
+
0: 'info', // 未提交
|
| 1442 |
+
1: 'primary', // 已提交
|
| 1443 |
+
2: 'danger', // 提交失败
|
| 1444 |
+
3: 'success' // 已提取链接
|
| 1445 |
+
};
|
| 1446 |
+
return typeMap[status] || 'info';
|
| 1447 |
+
};
|
| 1448 |
+
|
| 1449 |
+
// 工具函数 - 获取任务状态文本
|
| 1450 |
+
const getTaskStatusText = (status) => {
|
| 1451 |
+
const statusMap = {
|
| 1452 |
+
'running': '运行中',
|
| 1453 |
+
'completed': '已完成',
|
| 1454 |
+
'failed': '失败'
|
| 1455 |
+
};
|
| 1456 |
+
return statusMap[status] || status;
|
| 1457 |
+
};
|
| 1458 |
+
|
| 1459 |
+
// 工具函数 - 获取任务状态类型
|
| 1460 |
+
const getTaskStatusType = (status) => {
|
| 1461 |
+
const typeMap = {
|
| 1462 |
+
'running': 'primary',
|
| 1463 |
+
'completed': 'success',
|
| 1464 |
+
'failed': 'danger'
|
| 1465 |
+
};
|
| 1466 |
+
return typeMap[status] || 'info';
|
| 1467 |
+
};
|
| 1468 |
+
|
| 1469 |
+
// 工具函数 - 获取任务状态图标
|
| 1470 |
+
const getTaskStatusIcon = (status) => {
|
| 1471 |
+
const iconMap = {
|
| 1472 |
+
'running': 'Loading',
|
| 1473 |
+
'completed': 'Check',
|
| 1474 |
+
'failed': 'Close'
|
| 1475 |
+
};
|
| 1476 |
+
return iconMap[status] || '';
|
| 1477 |
+
};
|
| 1478 |
+
|
| 1479 |
+
// 工具函数 - 获取任务类型文本
|
| 1480 |
+
const getTaskTypeText = (type) => {
|
| 1481 |
+
const typeMap = {
|
| 1482 |
+
'submit': '提交邮箱',
|
| 1483 |
+
'extract': '提取链接'
|
| 1484 |
+
};
|
| 1485 |
+
return typeMap[type] || type;
|
| 1486 |
+
};
|
| 1487 |
+
|
| 1488 |
+
// 工具函数 - 获取操作文本
|
| 1489 |
+
const getOperationText = (operation) => {
|
| 1490 |
+
const operationMap = {
|
| 1491 |
+
'submit_email': '提交邮箱',
|
| 1492 |
+
'extract_link': '提取链接',
|
| 1493 |
+
'submit_email_background': '后台提交',
|
| 1494 |
+
'extract_link_background': '后台提取',
|
| 1495 |
+
'update_status': '更新状态'
|
| 1496 |
+
};
|
| 1497 |
+
return operationMap[operation] || operation;
|
| 1498 |
+
};
|
| 1499 |
+
|
| 1500 |
+
// 工具函数 - 获取日志记录文本
|
| 1501 |
+
const getMessageText = (message) => { // 获取日志记录文本
|
| 1502 |
+
//直接返回消息
|
| 1503 |
+
return message || '-';
|
| 1504 |
+
};
|
| 1505 |
+
// 工具函数 - 获取日志状态类型
|
| 1506 |
+
const getLogStatusType = (status) => {
|
| 1507 |
+
const typeMap = {
|
| 1508 |
+
'success': 'success',
|
| 1509 |
+
'failed': 'danger',
|
| 1510 |
+
'error': 'danger',
|
| 1511 |
+
'processing': 'warning'
|
| 1512 |
+
};
|
| 1513 |
+
return typeMap[status] || 'info';
|
| 1514 |
+
};
|
| 1515 |
+
|
| 1516 |
+
// 工具函数 - 处理状态筛选变化
|
| 1517 |
+
const handleStatusChange = (value) => {
|
| 1518 |
+
console.log('状态筛选变化:', value);
|
| 1519 |
+
};
|
| 1520 |
+
|
| 1521 |
+
return {
|
| 1522 |
+
activeTab,
|
| 1523 |
+
accounts,
|
| 1524 |
+
selectedAccounts,
|
| 1525 |
+
currentAccount,
|
| 1526 |
+
tasks,
|
| 1527 |
+
logs,
|
| 1528 |
+
statistics,
|
| 1529 |
+
pagination,
|
| 1530 |
+
logsPagination,
|
| 1531 |
+
accountsFilter,
|
| 1532 |
+
logsFilter,
|
| 1533 |
+
importForm,
|
| 1534 |
+
exportForm,
|
| 1535 |
+
statusUpdateForm,
|
| 1536 |
+
submitForm,
|
| 1537 |
+
extractForm,
|
| 1538 |
+
loginForm,
|
| 1539 |
+
dialogs,
|
| 1540 |
+
loading,
|
| 1541 |
+
|
| 1542 |
+
// 方法
|
| 1543 |
+
loadAccounts,
|
| 1544 |
+
loadStatistics,
|
| 1545 |
+
loadTasks,
|
| 1546 |
+
loadLogs,
|
| 1547 |
+
handleSizeChange,
|
| 1548 |
+
handleCurrentChange,
|
| 1549 |
+
handleLogsSizeChange,
|
| 1550 |
+
handleLogsCurrentChange,
|
| 1551 |
+
handleSelectionChange,
|
| 1552 |
+
handleTabClick,
|
| 1553 |
+
showImportDialog,
|
| 1554 |
+
handleImportFileChange,
|
| 1555 |
+
importAccounts,
|
| 1556 |
+
showExportDialog,
|
| 1557 |
+
exportAccounts,
|
| 1558 |
+
resetFailedAccounts,
|
| 1559 |
+
viewAccountDetails,
|
| 1560 |
+
updateAccountStatus,
|
| 1561 |
+
deleteAccount,
|
| 1562 |
+
submitSelectedAccounts,
|
| 1563 |
+
submitSingleAccount,
|
| 1564 |
+
confirmSubmit,
|
| 1565 |
+
extractSelectedAccounts,
|
| 1566 |
+
extractSingleAccount,
|
| 1567 |
+
confirmExtract,
|
| 1568 |
+
toggleAccountUsed,
|
| 1569 |
+
batchToggleUsed,
|
| 1570 |
+
copyToClipboard,
|
| 1571 |
+
handleLogin,
|
| 1572 |
+
logout,
|
| 1573 |
+
checkLogin,
|
| 1574 |
+
|
| 1575 |
+
// 工具函数
|
| 1576 |
+
getStatusText,
|
| 1577 |
+
getStatusType,
|
| 1578 |
+
getTaskStatusText,
|
| 1579 |
+
getTaskStatusType,
|
| 1580 |
+
getTaskStatusIcon,
|
| 1581 |
+
getTaskTypeText,
|
| 1582 |
+
getOperationText,
|
| 1583 |
+
getLogStatusType,
|
| 1584 |
+
getMessageText,
|
| 1585 |
+
handleStatusChange
|
| 1586 |
+
};
|
| 1587 |
+
}
|
| 1588 |
+
});
|
| 1589 |
+
|
| 1590 |
+
// // 全局注册Element Plus
|
| 1591 |
+
// for (const [key, component] of Object.entries(ElementPlus)) {
|
| 1592 |
+
// app.component(key, component);
|
| 1593 |
+
// }
|
| 1594 |
+
|
| 1595 |
+
// 注册所有图标
|
| 1596 |
+
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
| 1597 |
+
app.component(key, component);
|
| 1598 |
+
}
|
| 1599 |
+
|
| 1600 |
+
// 使用Element Plus
|
| 1601 |
+
app.use(ElementPlus);
|
| 1602 |
+
|
| 1603 |
+
app.mount('#app');
|
| 1604 |
+
</script>
|
| 1605 |
+
</body>
|
| 1606 |
+
</html>
|
jetbrains.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, Dict, Any, Tuple
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
import httpx
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from DrissionPage import Chromium
|
| 7 |
+
from DrissionPage._configs.chromium_options import ChromiumOptions
|
| 8 |
+
from DrissionPage._pages.mix_tab import MixTab
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class CaptchaConfig:
|
| 13 |
+
"""验证码解决配置"""
|
| 14 |
+
speech2text_api: str = os.environ.get("SPEECH2TEXT_API", "")
|
| 15 |
+
auth_key: str = os.environ.get("AUTH_KEY", "")
|
| 16 |
+
# auth_key: str = "your_auth_key_here" # 替换为你的API密钥
|
| 17 |
+
max_retries: int = 3
|
| 18 |
+
retry_delay: float = 1.0
|
| 19 |
+
page_load_timeout: float = 20.0
|
| 20 |
+
element_timeout: float = 5.0
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class CaptchaSolver:
|
| 24 |
+
def __init__(self, email: str, firstname, lastname, is_teacher: bool = False, config: Optional[CaptchaConfig] = None, proxy = None):
|
| 25 |
+
self.email = email
|
| 26 |
+
self.firstname = firstname
|
| 27 |
+
self.lastname = lastname
|
| 28 |
+
self.is_teacher = is_teacher
|
| 29 |
+
self.config = config or CaptchaConfig()
|
| 30 |
+
self.tab: Optional[MixTab] = None
|
| 31 |
+
self.proxy = proxy
|
| 32 |
+
|
| 33 |
+
def __enter__(self):
|
| 34 |
+
self.tab = self._init_browser()
|
| 35 |
+
return self
|
| 36 |
+
|
| 37 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
| 38 |
+
# return False # 允许异常传播
|
| 39 |
+
if self.tab:
|
| 40 |
+
self.tab.clear_cache()
|
| 41 |
+
self.tab.close()
|
| 42 |
+
self.tab.browser.quit()
|
| 43 |
+
|
| 44 |
+
def _init_browser(self) -> MixTab:
|
| 45 |
+
"""初始化浏览器并返回标签页"""
|
| 46 |
+
co = ChromiumOptions().set_paths(browser_path=r".\Chrome\chrome.exe")
|
| 47 |
+
co.incognito(on_off=True) # 无痕隐身模式打开的话。不会记住你的网站账号密码
|
| 48 |
+
# co.set_local_port("1236")
|
| 49 |
+
co.auto_port(on_off=True)
|
| 50 |
+
|
| 51 |
+
# co.headless(False)
|
| 52 |
+
co.headless(True)
|
| 53 |
+
co.set_user_agent(
|
| 54 |
+
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTM, like Gecko) chrome/124.0.0.8 safari/537.36')
|
| 55 |
+
# 阻止“自动保存密码”的提示气泡
|
| 56 |
+
co.set_pref('credentials_enable_service', False)
|
| 57 |
+
# 阻止“要恢复页面吗?Chrome未正确关闭”的提示气泡
|
| 58 |
+
co.set_argument('--hide-crash-restore-bubble')
|
| 59 |
+
co.set_proxy(proxy=self.proxy)
|
| 60 |
+
browser = Chromium(co)
|
| 61 |
+
browser.clear_cache()
|
| 62 |
+
tab = browser.new_tab()
|
| 63 |
+
tab.set.timeouts(page_load=self.config.page_load_timeout)
|
| 64 |
+
return tab
|
| 65 |
+
def _colese_browser(self):
|
| 66 |
+
"""关闭浏览器并清理资源"""
|
| 67 |
+
if self.tab:
|
| 68 |
+
self.tab.clear_cache()
|
| 69 |
+
self.tab.close()
|
| 70 |
+
self.tab.browser.quit()
|
| 71 |
+
def _wait_for_element(self, selector: str, timeout: Optional[float] = None) -> Any:
|
| 72 |
+
"""等待元素出现"""
|
| 73 |
+
timeout = timeout or self.config.element_timeout
|
| 74 |
+
return self.tab.ele(selector, timeout=timeout)
|
| 75 |
+
|
| 76 |
+
def get_audio_data(self) -> Tuple[Optional[str], bool]:
|
| 77 |
+
"""
|
| 78 |
+
获取音频验证码数据
|
| 79 |
+
返回: (audio_data, needs_captcha)
|
| 80 |
+
"""
|
| 81 |
+
try:
|
| 82 |
+
self.tab.get('https://www.jetbrains.com/shop/eform/students')
|
| 83 |
+
|
| 84 |
+
# 检查是否需要验证码
|
| 85 |
+
if "Human Verification" not in self.tab.title:
|
| 86 |
+
print("页面不需要人机验证")
|
| 87 |
+
return None, False
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
# time.sleep(2) # 等待页面加载
|
| 91 |
+
self.tab.wait.eles_loaded('#amzn-captcha-verify-button')
|
| 92 |
+
verify_btn = self._wait_for_element('#amzn-captcha-verify-button')
|
| 93 |
+
verify_btn.click(by_js=True)
|
| 94 |
+
except Exception:
|
| 95 |
+
# 如果找不到验证按钮,可能已经通过验证
|
| 96 |
+
if self._wait_for_element('@name=email', timeout=2):
|
| 97 |
+
print("已经不需要人机验证")
|
| 98 |
+
return None, False
|
| 99 |
+
raise
|
| 100 |
+
|
| 101 |
+
# 设置监听并点击音频按钮
|
| 102 |
+
self.tab.listen.start('problem?kind=audio')
|
| 103 |
+
audio_btn = self._wait_for_element('#amzn-btn-audio-internal')
|
| 104 |
+
audio_btn.click(by_js=True)
|
| 105 |
+
|
| 106 |
+
# 等待并获取音频数据
|
| 107 |
+
for _ in range(self.config.max_retries):
|
| 108 |
+
res = self.tab.listen.wait()
|
| 109 |
+
try:
|
| 110 |
+
audio_data = res.response.body
|
| 111 |
+
return audio_data['assets']['audio'], True
|
| 112 |
+
except (json.JSONDecodeError, KeyError):
|
| 113 |
+
print("数据包解析失败,继续等待...")
|
| 114 |
+
time.sleep(self.config.retry_delay)
|
| 115 |
+
|
| 116 |
+
raise TimeoutError("获取音频数据超时")
|
| 117 |
+
|
| 118 |
+
except Exception as e:
|
| 119 |
+
print(f"获取音频数据失败: {str(e)}")
|
| 120 |
+
return None, False
|
| 121 |
+
|
| 122 |
+
def call_whisper_api(self, audio_base64: str) -> Optional[Dict[str, Any]]:
|
| 123 |
+
"""调用Whisper API进行语音识别"""
|
| 124 |
+
print("开始识别人机验��码")
|
| 125 |
+
|
| 126 |
+
headers = {
|
| 127 |
+
"Content-Type": "application/json",
|
| 128 |
+
"Accept": "application/json",
|
| 129 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.8 Safari/537.36"
|
| 130 |
+
}
|
| 131 |
+
if self.config.auth_key:
|
| 132 |
+
headers["Authorization"] = f"Bearer {self.config.auth_key}"
|
| 133 |
+
payload = {
|
| 134 |
+
"audio": audio_base64,
|
| 135 |
+
"task": "translate"
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
try:
|
| 139 |
+
with httpx.Client(timeout=20) as client:
|
| 140 |
+
response = client.post(
|
| 141 |
+
self.config.speech2text_api,
|
| 142 |
+
headers=headers,
|
| 143 |
+
json=payload
|
| 144 |
+
)
|
| 145 |
+
response.raise_for_status()
|
| 146 |
+
return response.json()
|
| 147 |
+
except httpx.HTTPError as e:
|
| 148 |
+
print(f"API请求失败: {str(e)}")
|
| 149 |
+
return None
|
| 150 |
+
except json.JSONDecodeError:
|
| 151 |
+
print("API响应解析失败")
|
| 152 |
+
return None
|
| 153 |
+
|
| 154 |
+
@staticmethod
|
| 155 |
+
def parse_result(result: Dict[str, Any]) -> str:
|
| 156 |
+
"""解析语音识别结果"""
|
| 157 |
+
# if not result or 'segments' not in result or not result['segments']:
|
| 158 |
+
# raise ValueError("无效的API响应: 缺少segments字段")
|
| 159 |
+
|
| 160 |
+
# last_segment = result['segments'][-1].get('text', '').strip()
|
| 161 |
+
if 'text' not in result:
|
| 162 |
+
raise ValueError("无效的API响应: 缺少text字段")
|
| 163 |
+
last_segment = result.get('text', '').strip()
|
| 164 |
+
if not last_segment:
|
| 165 |
+
raise ValueError("无法从结果中提取文本")
|
| 166 |
+
|
| 167 |
+
# 清理结果: 取最后一个词,移除空格和标点
|
| 168 |
+
last_word = last_segment.split()[-1]
|
| 169 |
+
return last_word.replace(' ', '').replace('.', '').replace(',', '')
|
| 170 |
+
|
| 171 |
+
def fill_form(self) -> bool:
|
| 172 |
+
"""填写并提交表单"""
|
| 173 |
+
try:
|
| 174 |
+
# 等待表单元素出现
|
| 175 |
+
self.tab.wait.eles_loaded('@name=email')
|
| 176 |
+
email_input = self._wait_for_element('@name=email')
|
| 177 |
+
email_input.input(self.email, clear=True)
|
| 178 |
+
|
| 179 |
+
role_value = "TEACHER" if self.is_teacher else "STUDENT"
|
| 180 |
+
self._wait_for_element(f'@@name=studentType@@value={role_value}').click(by_js=True)
|
| 181 |
+
self._wait_for_element('@name=name.firstName').input(self.firstname, clear=True)
|
| 182 |
+
self._wait_for_element('@name=name.lastName').input(self.lastname, clear=True)
|
| 183 |
+
self._wait_for_element('@name=privacyPolicy').click(by_js=True)
|
| 184 |
+
|
| 185 |
+
submit_btn = self._wait_for_element('xpath://button[@type="submit"]')
|
| 186 |
+
submit_btn.click(by_js=True)
|
| 187 |
+
|
| 188 |
+
return True
|
| 189 |
+
except Exception as e:
|
| 190 |
+
print(f"表单填写失败: {str(e)}")
|
| 191 |
+
return False
|
| 192 |
+
def check_success(self) -> bool:
|
| 193 |
+
"""检查表单提交是否成功"""
|
| 194 |
+
try:
|
| 195 |
+
if "primaryConfirmation?email=" in self.tab.url:
|
| 196 |
+
print(f"{self.email} 提交成功")
|
| 197 |
+
return True
|
| 198 |
+
else:
|
| 199 |
+
print(f"{self.email} 提交失败")
|
| 200 |
+
return False
|
| 201 |
+
except Exception as e:
|
| 202 |
+
print(f"检查提交状态失败:{self.email} {str(e)}")
|
| 203 |
+
return False
|
| 204 |
+
|
| 205 |
+
def solve_audio_captcha(self) -> bool | None:
|
| 206 |
+
"""主流程: 解决音频验证码并提交表单"""
|
| 207 |
+
try:
|
| 208 |
+
while True:
|
| 209 |
+
# 1. 获取音频数据
|
| 210 |
+
print(f"{self.email} 正在获取音频验证码")
|
| 211 |
+
audio_data, needs_captcha = self.get_audio_data()
|
| 212 |
+
|
| 213 |
+
if needs_captcha and audio_data:
|
| 214 |
+
# 2. 调用API识别
|
| 215 |
+
result = self.call_whisper_api(audio_data)
|
| 216 |
+
if not result:
|
| 217 |
+
raise RuntimeError("语音识别API调用失败")
|
| 218 |
+
print(result)
|
| 219 |
+
# 3. 解析结果
|
| 220 |
+
last_word = self.parse_result(result)
|
| 221 |
+
print(f"识别结果: {last_word}")
|
| 222 |
+
|
| 223 |
+
# 4. 提交验证码
|
| 224 |
+
input_field = self._wait_for_element('tag:input')
|
| 225 |
+
input_field.input(last_word)
|
| 226 |
+
time.sleep(1) # 短暂等待确保输入完成
|
| 227 |
+
|
| 228 |
+
verify_btn = self._wait_for_element('#amzn-btn-verify-internal')
|
| 229 |
+
verify_btn.click(by_js=True)
|
| 230 |
+
time.sleep(2) # 等待验证结果
|
| 231 |
+
if "Human Verification" in self.tab.title:
|
| 232 |
+
print("验证码验证失败,重试")
|
| 233 |
+
continue
|
| 234 |
+
# 5. 填写表单
|
| 235 |
+
if not self.fill_form():
|
| 236 |
+
raise RuntimeError("表单填写失败")
|
| 237 |
+
# 6. 检查提交状态
|
| 238 |
+
time.sleep(1) # 等待页面加载
|
| 239 |
+
if not self.check_success():
|
| 240 |
+
raise RuntimeError("表单提交失败")
|
| 241 |
+
print("验证流程完成")
|
| 242 |
+
return True
|
| 243 |
+
|
| 244 |
+
except Exception as e:
|
| 245 |
+
print(f"处理过程中发生错误: {str(e)}")
|
| 246 |
+
return False
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
if __name__ == "__main__":
|
| 250 |
+
# 示例用法
|
| 251 |
+
with CaptchaSolver(email="[email protected]",
|
| 252 |
+
firstname="John",
|
| 253 |
+
lastname="Doe",
|
| 254 |
+
is_teacher=True,
|
| 255 |
+
) as solver:
|
| 256 |
+
success = solver.solve_audio_captcha()
|
| 257 |
+
if not success:
|
| 258 |
+
exit(1)
|
unibo_auth2_get_AuthCode_RefreshToken_sucsses.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import concurrent.futures
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import threading
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
import httpx
|
| 9 |
+
from bs4 import BeautifulSoup
|
| 10 |
+
from urllib.parse import urljoin, urlencode
|
| 11 |
+
from loguru import logger
|
| 12 |
+
|
| 13 |
+
# logger.stop()
|
| 14 |
+
# 定义线程安全计数器和锁
|
| 15 |
+
success_counter = 0
|
| 16 |
+
error_counter = 0
|
| 17 |
+
# 定义文件名 filename = user-当前月份-日期.txt
|
| 18 |
+
current_date = datetime.now().strftime("%m-%d")
|
| 19 |
+
# filename = f'user-{current_date}.txt'
|
| 20 |
+
counter_lock = threading.Lock()
|
| 21 |
+
success_file_lock = threading.Lock()
|
| 22 |
+
error_file_lock = threading.Lock()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class OAuth2Authenticator:
|
| 26 |
+
def __init__(self, username, password):
|
| 27 |
+
self.username = username
|
| 28 |
+
self.password = password
|
| 29 |
+
self.client_id = '9e5f94bc-e8a4-4e73-b8be-63364c29d753'
|
| 30 |
+
self.session = httpx.Client(timeout=30.0, follow_redirects=True, verify=False)
|
| 31 |
+
self.base_urls = {
|
| 32 |
+
'microsoft': 'https://login.microsoftonline.com',
|
| 33 |
+
'idp': 'https://idp.unibo.it'
|
| 34 |
+
}
|
| 35 |
+
self.current_state = {} # 用于存储流程中的临时数据
|
| 36 |
+
|
| 37 |
+
def _extract_input_value(self, html, name):
|
| 38 |
+
"""从HTML中提取指定名称的input值"""
|
| 39 |
+
soup = BeautifulSoup(html, 'html.parser')
|
| 40 |
+
element = soup.find('input', {'name': name})
|
| 41 |
+
return element['value'] if element else None
|
| 42 |
+
|
| 43 |
+
def _make_request(self, method, url, data=None, **kwargs):
|
| 44 |
+
"""封装请求方法,统一处理异常"""
|
| 45 |
+
try:
|
| 46 |
+
# 设置默认的请求头
|
| 47 |
+
self.session.headers = {
|
| 48 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Thunderbird/137.0',
|
| 49 |
+
# 'Accept': 'application/json, text/javascript, */*; q=0.01',
|
| 50 |
+
# 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
| 51 |
+
# 'X-Requested-With': 'XMLHttpRequest'
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
response = self.session.request(method, url, data=data, **kwargs)
|
| 55 |
+
url = response.url
|
| 56 |
+
print(f"请求的URL: {url}")
|
| 57 |
+
# response.raise_for_status()
|
| 58 |
+
return response
|
| 59 |
+
except httpx.ConnectError as e:
|
| 60 |
+
# print(f"连接失败的目标URL: {e.request.url}")
|
| 61 |
+
# print(f"错误详情: {e}")
|
| 62 |
+
raise ValueError(e.request.url)
|
| 63 |
+
except Exception as e:
|
| 64 |
+
print(f"其他错误: {e}")
|
| 65 |
+
raise
|
| 66 |
+
|
| 67 |
+
def _build_auth_url(self):
|
| 68 |
+
"""构建初始认证URL"""
|
| 69 |
+
params = {
|
| 70 |
+
'response_type': 'code',
|
| 71 |
+
'client_id': self.client_id,
|
| 72 |
+
'redirect_uri': 'https://localhost',
|
| 73 |
+
'scope': 'https://outlook.office.com/EWS.AccessAsUser.All https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access',
|
| 74 |
+
'login_hint': self.username
|
| 75 |
+
}
|
| 76 |
+
return f"{self.base_urls['microsoft']}/common/oauth2/v2.0/authorize?{urlencode(params)}"
|
| 77 |
+
|
| 78 |
+
def step1_get_initial_login_page(self):
|
| 79 |
+
"""第一步: 获取初始登录页面并提取SAML参数"""
|
| 80 |
+
logger.info("执行第一步: 获取初始登录页面")
|
| 81 |
+
auth_url = self._build_auth_url()
|
| 82 |
+
response = self._make_request('GET', auth_url)
|
| 83 |
+
|
| 84 |
+
# self.current_state['relay_state'] = self._extract_input_value(response.text, 'RelayState')
|
| 85 |
+
# self.current_state['saml_request'] = self._extract_input_value(response.text, 'SAMLRequest')
|
| 86 |
+
|
| 87 |
+
# if not all([self.current_state['relay_state'], self.current_state['saml_request']]):
|
| 88 |
+
# raise ValueError("无法从登录页面提取必要的SAML参数")
|
| 89 |
+
config_match = re.search(r'\$Config=({.*?//])', response.text, re.DOTALL)
|
| 90 |
+
if config_match:
|
| 91 |
+
config_value = config_match.group(1).replace('//]', '').strip()
|
| 92 |
+
self.current_state['config'] = json.loads(config_value[:-1])
|
| 93 |
+
else:
|
| 94 |
+
raise ValueError("无法从响应中提取配置信息")
|
| 95 |
+
return response
|
| 96 |
+
|
| 97 |
+
def step2_submit_saml_request(self):
|
| 98 |
+
"""第二步: 提交SAML请求到IDP"""
|
| 99 |
+
logger.info("执行第二步: 提交SAML请求到IDP")
|
| 100 |
+
|
| 101 |
+
data = {
|
| 102 |
+
"UserName": self.username,
|
| 103 |
+
"Password": self.password,
|
| 104 |
+
"AuthMethod": "FormsAuthentication"
|
| 105 |
+
}
|
| 106 |
+
idp_sso_url = self.current_state['config'].get('bsso').get('failureRedirectUrl')
|
| 107 |
+
logger.info(f"IDP SSO URL: {idp_sso_url}")
|
| 108 |
+
response = self._make_request('POST', idp_sso_url, data=data)
|
| 109 |
+
# 提取配置信息
|
| 110 |
+
logger.info(f"LAST URL: {response.url}")
|
| 111 |
+
|
| 112 |
+
self.current_state['wa'] = self._extract_input_value(response.text, 'wa')
|
| 113 |
+
self.current_state['wresult'] = self._extract_input_value(response.text, 'wresult')
|
| 114 |
+
self.current_state['wctx'] = self._extract_input_value(response.text, 'wctx')
|
| 115 |
+
# if not self.current_state['csrf_token']:
|
| 116 |
+
# raise ValueError("无法从响应中提取CSRF令牌")
|
| 117 |
+
|
| 118 |
+
return response
|
| 119 |
+
|
| 120 |
+
def step3_submit_loginsrf_response(self):
|
| 121 |
+
"""第三步: 提交登录响应"""
|
| 122 |
+
logger.info("执行第三步: 提交登录响应")
|
| 123 |
+
|
| 124 |
+
response = self._make_request('POST',
|
| 125 |
+
urljoin(self.base_urls['microsoft'], '/login.srf'),
|
| 126 |
+
data={
|
| 127 |
+
"wa": self.current_state['wa'],
|
| 128 |
+
"wresult": self.current_state['wresult'],
|
| 129 |
+
"wctx": self.current_state['wctx']
|
| 130 |
+
}
|
| 131 |
+
)
|
| 132 |
+
config_match = re.search(r'\$Config=({.*?//])', response.text, re.DOTALL)
|
| 133 |
+
if config_match:
|
| 134 |
+
config_value = config_match.group(1).replace('//]', '').strip()
|
| 135 |
+
self.current_state['config'] = json.loads(config_value[:-1])
|
| 136 |
+
else:
|
| 137 |
+
raise ValueError("无法从响应中提取配置信息")
|
| 138 |
+
return response
|
| 139 |
+
|
| 140 |
+
def step6_submit_saml_response(self):
|
| 141 |
+
"""第六步: 处理授权同意"""
|
| 142 |
+
logger.info("执行第六步: 处理授权同意")
|
| 143 |
+
if 'config' not in self.current_state:
|
| 144 |
+
raise ValueError("缺少配置信息")
|
| 145 |
+
|
| 146 |
+
config = self.current_state['config']
|
| 147 |
+
response = self._make_request('POST',
|
| 148 |
+
urljoin(self.base_urls['microsoft'], '/appverify'),
|
| 149 |
+
data={
|
| 150 |
+
"ContinueAuth": True,
|
| 151 |
+
"ctx": config.get("sCtx"),
|
| 152 |
+
"hpgrequestid": config.get("sessionId"),
|
| 153 |
+
"flowToken": config.get("sFT"),
|
| 154 |
+
"iscsrfspeedbump": True,
|
| 155 |
+
"canary": config.get("canary"),
|
| 156 |
+
"i19": 492026
|
| 157 |
+
}
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# 提取配置信息
|
| 161 |
+
config_match = re.search(r'\$Config=({.*?//])', response.text, re.DOTALL)
|
| 162 |
+
if config_match:
|
| 163 |
+
config_value = config_match.group(1).replace('//]', '').strip()
|
| 164 |
+
self.current_state['config'] = json.loads(config_value[:-1])
|
| 165 |
+
else:
|
| 166 |
+
raise ValueError("无法从响应中提取配置信息")
|
| 167 |
+
|
| 168 |
+
return response
|
| 169 |
+
|
| 170 |
+
def step7_handle_consent(self):
|
| 171 |
+
"""第七步: 处理授权同意"""
|
| 172 |
+
logger.info("执行第七步: 处理授权同意")
|
| 173 |
+
if 'config' not in self.current_state:
|
| 174 |
+
raise ValueError("缺少配置信息")
|
| 175 |
+
|
| 176 |
+
config = self.current_state['config']
|
| 177 |
+
response = self._make_request('POST',
|
| 178 |
+
urljoin(self.base_urls['microsoft'], '/common/Consent/Set'),
|
| 179 |
+
data={
|
| 180 |
+
"acceptConsent": True,
|
| 181 |
+
"ctx": config.get("sCtx"),
|
| 182 |
+
"hpgrequestid": config.get("sessionId"),
|
| 183 |
+
"flowToken": config.get("sFT"),
|
| 184 |
+
"canary": config.get("canary"),
|
| 185 |
+
"i19": 958761
|
| 186 |
+
}
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
if 'localhost/?code=' in response.url:
|
| 190 |
+
self.current_state['auth_code'] = self._extract_auth_code(response.url)
|
| 191 |
+
return response
|
| 192 |
+
|
| 193 |
+
raise ValueError("未能成功获取授权码")
|
| 194 |
+
|
| 195 |
+
def _extract_auth_code(self, url):
|
| 196 |
+
"""从URL中提取授权码"""
|
| 197 |
+
logger.info(f'提取授权码: {url}')
|
| 198 |
+
url = str(url)
|
| 199 |
+
match = re.search(r'code=([^&]+)', url)
|
| 200 |
+
return match.group(1) if match else None
|
| 201 |
+
|
| 202 |
+
def get_refresh_token(self, code):
|
| 203 |
+
"""获取刷新令牌"""
|
| 204 |
+
url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
| 205 |
+
data = {
|
| 206 |
+
'client_id': self.client_id,
|
| 207 |
+
'grant_type': 'authorization_code',
|
| 208 |
+
'redirect_uri': 'https://localhost',
|
| 209 |
+
'code': code
|
| 210 |
+
}
|
| 211 |
+
response = self._make_request('POST', url, data=data)
|
| 212 |
+
response_data = response.json()
|
| 213 |
+
if 'error' in response_data:
|
| 214 |
+
raise ValueError(f"获取刷新令牌失败: {response_data['error_description']}")
|
| 215 |
+
refresh_token = response_data.get('refresh_token')
|
| 216 |
+
access_token = response_data.get('access_token')
|
| 217 |
+
return refresh_token, access_token
|
| 218 |
+
|
| 219 |
+
def execute_flow(self):
|
| 220 |
+
"""执行完整的OAuth2认证流程"""
|
| 221 |
+
try:
|
| 222 |
+
# 更新每一步的last_url
|
| 223 |
+
response = self.step1_get_initial_login_page()
|
| 224 |
+
self.current_state['last_url'] = response.url
|
| 225 |
+
response = self.step2_submit_saml_request()
|
| 226 |
+
self.current_state['last_url'] = response.url
|
| 227 |
+
response = self.step3_submit_loginsrf_response()
|
| 228 |
+
self.current_state['last_url'] = response.url
|
| 229 |
+
response = self.step6_submit_saml_response()
|
| 230 |
+
self.current_state['last_url'] = response.url
|
| 231 |
+
|
| 232 |
+
response = self.step7_handle_consent()
|
| 233 |
+
self.current_state['last_url'] = response.url
|
| 234 |
+
|
| 235 |
+
if 'auth_code' in self.current_state:
|
| 236 |
+
return self.current_state['auth_code']
|
| 237 |
+
|
| 238 |
+
raise Exception("认证流程未完成")
|
| 239 |
+
|
| 240 |
+
except Exception as e:
|
| 241 |
+
code = self._extract_auth_code(e)
|
| 242 |
+
if code:
|
| 243 |
+
# logger.info(f"提取到的授权码: {code}")
|
| 244 |
+
return code
|
| 245 |
+
else:
|
| 246 |
+
logger.info(f"认证流程出错: {e}")
|
| 247 |
+
logger.info("未能提取到授权码")
|
| 248 |
+
raise
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def handle_success(username, original_line_new):
|
| 252 |
+
"""处理成功账号,线程安全地写入文件并更新计数器"""
|
| 253 |
+
global success_counter
|
| 254 |
+
try:
|
| 255 |
+
with success_file_lock:
|
| 256 |
+
with open(success_file, 'a', encoding='utf-8') as file:
|
| 257 |
+
file.write(original_line_new + '\n')
|
| 258 |
+
|
| 259 |
+
with counter_lock:
|
| 260 |
+
success_counter += 1
|
| 261 |
+
current_count = success_counter
|
| 262 |
+
|
| 263 |
+
logger.info(f"{username} 已写入成功文件,当前成功数: {success_counter},当前失败数: {error_counter}")
|
| 264 |
+
except Exception as e:
|
| 265 |
+
logger.error(f"写入成功账号 {username} 时出错: {str(e)}")
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def handle_failure(username, original_line):
|
| 269 |
+
"""处理失败账号,线程安全地写入文件并更新计数器"""
|
| 270 |
+
global error_counter
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
with error_file_lock:
|
| 274 |
+
with open(error_file, 'a', encoding='utf-8') as file:
|
| 275 |
+
file.write(original_line + '\n')
|
| 276 |
+
|
| 277 |
+
with counter_lock:
|
| 278 |
+
error_counter += 1
|
| 279 |
+
current_count = error_counter
|
| 280 |
+
|
| 281 |
+
logger.info(f"{username} 已写入失败文件,当前成功数: {success_counter},当前失败数: {error_counter}")
|
| 282 |
+
except Exception as e:
|
| 283 |
+
logger.error(f"写入失败账号 {username} 时出错: {str(e)}")
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def read_user_credentials(filepath='user.txt'):
|
| 287 |
+
"""从user.txt文件中读取用户名和密码"""
|
| 288 |
+
credentials = []
|
| 289 |
+
try:
|
| 290 |
+
with open(filepath, 'r', encoding='utf-8') as file:
|
| 291 |
+
for line in file:
|
| 292 |
+
parts = line.strip().split('---')
|
| 293 |
+
if len(parts) >= 3: # 至少需要时间、用户名和密码
|
| 294 |
+
timestamp = parts[0]
|
| 295 |
+
username = parts[1]
|
| 296 |
+
password = parts[2]
|
| 297 |
+
email = parts[3] if len(parts) > 3 else ""
|
| 298 |
+
credentials.append((username, password, email, line.strip()))
|
| 299 |
+
logger.info(f"成功从{filepath}读取了{len(credentials)}个账号信息")
|
| 300 |
+
return credentials
|
| 301 |
+
except Exception as e:
|
| 302 |
+
logger.error(f"读取凭据文件时出错: {str(e)}")
|
| 303 |
+
return []
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def ensure_files_exist():
|
| 307 |
+
"""确保输出文件存在"""
|
| 308 |
+
|
| 309 |
+
files = [success_file, error_file]
|
| 310 |
+
for file in files:
|
| 311 |
+
try:
|
| 312 |
+
if not os.path.exists(file):
|
| 313 |
+
with open(file, 'w', encoding='utf-8') as f:
|
| 314 |
+
pass
|
| 315 |
+
logger.info(f"创建文件 {file}")
|
| 316 |
+
except Exception as e:
|
| 317 |
+
logger.error(f"创建文件 {file} 时出错: {str(e)}")
|
| 318 |
+
return False
|
| 319 |
+
return True
|
| 320 |
+
def main_threaded(username, password, original_line):
|
| 321 |
+
authenticator = OAuth2Authenticator(
|
| 322 |
+
username=username,
|
| 323 |
+
password=password, # 实际使用时应从安全来源获取密码
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
try:
|
| 327 |
+
auth_code = authenticator.execute_flow()
|
| 328 |
+
refresh_token, access_token = authenticator.get_refresh_token(auth_code)
|
| 329 |
+
print(f"成功获取授权码:\n{auth_code}\n{refresh_token}\n{access_token}\n")
|
| 330 |
+
# 报错
|
| 331 |
+
original_line_new = f'{original_line}---{refresh_token}'
|
| 332 |
+
handle_success(username, original_line_new)
|
| 333 |
+
except Exception as e:
|
| 334 |
+
print(f"认证失败: {e}")
|
| 335 |
+
handle_failure(username, original_line)
|
| 336 |
+
|
| 337 |
+
# 使用示例
|
| 338 |
+
if __name__ == "__main__":
|
| 339 |
+
|
| 340 |
+
# authenticator = OAuth2Authenticator(
|
| 341 |
+
# username='[email protected]',
|
| 342 |
+
# password='W17M&HQK^x1q7h.', # 实际使用时应从安全来源获取密码
|
| 343 |
+
# )
|
| 344 |
+
#
|
| 345 |
+
# try:
|
| 346 |
+
# auth_code = authenticator.execute_flow()
|
| 347 |
+
# refresh_token, access_token = authenticator.get_refresh_token(auth_code)
|
| 348 |
+
# print(f"成功获取授权码:\n{auth_code}\n{refresh_token}\n{access_token}\n")
|
| 349 |
+
# except Exception as e:
|
| 350 |
+
# print(f"认证失败: {e}")
|
| 351 |
+
filename = 'user-4-21-success.txt'
|
| 352 |
+
success_file = filename.replace('success.txt', 'refresh-success.txt')
|
| 353 |
+
error_file = filename.replace('error.txt', 'refresh-error.txt')
|
| 354 |
+
filepath = filename
|
| 355 |
+
credentials = read_user_credentials(filepath)
|
| 356 |
+
if not credentials:
|
| 357 |
+
logger.error("没有读取到有效凭据,退出程序")
|
| 358 |
+
exit(1)
|
| 359 |
+
|
| 360 |
+
# 确保输出文件存在
|
| 361 |
+
if not ensure_files_exist():
|
| 362 |
+
logger.error("创建输出文件失败,退出程序")
|
| 363 |
+
exit(1)
|
| 364 |
+
|
| 365 |
+
# 设置最大线程数
|
| 366 |
+
max_workers = 1 # 可以根据需要调整线程数量
|
| 367 |
+
|
| 368 |
+
logger.info(f"开始多线程处理账号,最大并发数: {max_workers}")
|
| 369 |
+
|
| 370 |
+
# 使用线程池处理账号
|
| 371 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 372 |
+
# 提交所有任务
|
| 373 |
+
futures = []
|
| 374 |
+
for username, password, email, original_line in credentials:
|
| 375 |
+
# 提交任务到线程池
|
| 376 |
+
future = executor.submit(
|
| 377 |
+
main_threaded,
|
| 378 |
+
username,
|
| 379 |
+
password,
|
| 380 |
+
original_line
|
| 381 |
+
)
|
| 382 |
+
futures.append(future)
|
| 383 |
+
|
| 384 |
+
# 等待所有任务完成
|
| 385 |
+
for future in concurrent.futures.as_completed(futures):
|
| 386 |
+
try:
|
| 387 |
+
future.result() # 获取结果,但我们不需要处理
|
| 388 |
+
except Exception as e:
|
| 389 |
+
logger.error(f"执行任务时发生异常: {str(e)}")
|
| 390 |
+
|
| 391 |
+
logger.info(f"处理完成,共成功 {success_counter} 个账号,失败 {error_counter} 个账号。")
|
unibo_jetbrains_activation.py
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import time
|
| 7 |
+
import random
|
| 8 |
+
import sqlite3
|
| 9 |
+
import threading
|
| 10 |
+
import concurrent.futures
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from typing import Optional, Tuple, List, Dict, Any
|
| 13 |
+
|
| 14 |
+
from loguru import logger
|
| 15 |
+
from jetbrains import CaptchaSolver
|
| 16 |
+
from 收发邮件 import EmailClient
|
| 17 |
+
|
| 18 |
+
# 定义数据库状态常量
|
| 19 |
+
STATUS_PENDING = 0 # 未提交
|
| 20 |
+
STATUS_SUBMITTED = 1 # 已提交成功
|
| 21 |
+
STATUS_FAILED = 2 # 提交失败
|
| 22 |
+
STATUS_LINK_EXTRACTED = 3 # 已提取激活链接
|
| 23 |
+
|
| 24 |
+
class DatabaseManager:
|
| 25 |
+
"""数据库操作类,处理SQLite数据库的所有操作"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, db_path: str = "unibo_jetbrains.db"):
|
| 28 |
+
"""初始化数据库连接和表结构
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
db_path: 数据库文件路径
|
| 32 |
+
"""
|
| 33 |
+
self.db_path = db_path
|
| 34 |
+
self.conn = None
|
| 35 |
+
self.lock = threading.Lock() # 用于线程安全的数据库操作
|
| 36 |
+
self._initialize_db()
|
| 37 |
+
|
| 38 |
+
def _initialize_db(self) -> None:
|
| 39 |
+
"""初始化数据库,创建必要的表结构"""
|
| 40 |
+
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
| 41 |
+
cursor = self.conn.cursor()
|
| 42 |
+
|
| 43 |
+
# 创建邮箱数据表
|
| 44 |
+
cursor.execute('''
|
| 45 |
+
CREATE TABLE IF NOT EXISTS accounts (
|
| 46 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 47 |
+
register_time TEXT NOT NULL,
|
| 48 |
+
username TEXT NOT NULL UNIQUE,
|
| 49 |
+
password TEXT NOT NULL,
|
| 50 |
+
security_email TEXT,
|
| 51 |
+
status INTEGER DEFAULT 0,
|
| 52 |
+
activation_link TEXT,
|
| 53 |
+
updated_at TEXT,
|
| 54 |
+
notes TEXT,
|
| 55 |
+
used INTEGER DEFAULT 0 -- 新增字段,0未使用,1已使用
|
| 56 |
+
)
|
| 57 |
+
''')
|
| 58 |
+
|
| 59 |
+
# 创建日志表
|
| 60 |
+
cursor.execute('''
|
| 61 |
+
CREATE TABLE IF NOT EXISTS operation_logs (
|
| 62 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 63 |
+
username TEXT NOT NULL,
|
| 64 |
+
operation TEXT NOT NULL,
|
| 65 |
+
status TEXT NOT NULL,
|
| 66 |
+
message TEXT,
|
| 67 |
+
created_at TEXT NOT NULL
|
| 68 |
+
)
|
| 69 |
+
''')
|
| 70 |
+
|
| 71 |
+
self.conn.commit()
|
| 72 |
+
logger.info(f"数据库初始化完成: {self.db_path}")
|
| 73 |
+
|
| 74 |
+
def __del__(self):
|
| 75 |
+
"""析构函数,确保数据库连接正确关闭"""
|
| 76 |
+
if self.conn:
|
| 77 |
+
self.conn.close()
|
| 78 |
+
|
| 79 |
+
def import_from_file(self, filepath: str) -> int:
|
| 80 |
+
"""从文件导入账号数据
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
filepath: 包含账号数据的文件路径
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
导入的账号数量
|
| 87 |
+
"""
|
| 88 |
+
count = 0
|
| 89 |
+
try:
|
| 90 |
+
with self.lock, open(filepath, 'r', encoding='utf-8') as file:
|
| 91 |
+
cursor = self.conn.cursor()
|
| 92 |
+
|
| 93 |
+
for line in file:
|
| 94 |
+
parts = line.strip().split('---')
|
| 95 |
+
if len(parts) >= 3: # 至少需要时间、用户名和密码
|
| 96 |
+
register_time = parts[0]
|
| 97 |
+
username = parts[1]
|
| 98 |
+
password = parts[2]
|
| 99 |
+
security_email = parts[3] if len(parts) > 3 else ""
|
| 100 |
+
status = 0 # 默认为未提交状态
|
| 101 |
+
activation_link = ""
|
| 102 |
+
|
| 103 |
+
# 检查更多部分以确定状态和激活链接
|
| 104 |
+
if len(parts) > 4 and "success" in parts[4].lower():
|
| 105 |
+
status = STATUS_SUBMITTED
|
| 106 |
+
|
| 107 |
+
if len(parts) > 5 and "http" in parts[5].lower():
|
| 108 |
+
activation_link = parts[5]
|
| 109 |
+
status = STATUS_LINK_EXTRACTED
|
| 110 |
+
|
| 111 |
+
# 插入或更新数据库
|
| 112 |
+
try:
|
| 113 |
+
cursor.execute('''
|
| 114 |
+
INSERT OR IGNORE INTO accounts
|
| 115 |
+
(register_time, username, password, security_email, status, activation_link, updated_at)
|
| 116 |
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))
|
| 117 |
+
''', (register_time, username, password, security_email, status, activation_link))
|
| 118 |
+
|
| 119 |
+
if cursor.rowcount > 0:
|
| 120 |
+
count += 1
|
| 121 |
+
except sqlite3.IntegrityError:
|
| 122 |
+
logger.warning(f"账号已存在,跳过: {username}")
|
| 123 |
+
|
| 124 |
+
self.conn.commit()
|
| 125 |
+
logger.info(f"成功从{filepath}导入了{count}个账号")
|
| 126 |
+
return count
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.error(f"从文件导入账号时出错: {str(e)}")
|
| 129 |
+
return 0
|
| 130 |
+
|
| 131 |
+
def get_pending_accounts(self, limit: int = 10) -> List[Dict[str, Any]]:
|
| 132 |
+
"""获取待处理的账号(未提交状态)
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
limit: 返回账号的最大数量
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
账号列表,每个账号是一个字典
|
| 139 |
+
"""
|
| 140 |
+
with self.lock:
|
| 141 |
+
cursor = self.conn.cursor()
|
| 142 |
+
cursor.execute('''
|
| 143 |
+
SELECT id, register_time, username, password, security_email, status, activation_link
|
| 144 |
+
FROM accounts
|
| 145 |
+
WHERE status = ?
|
| 146 |
+
LIMIT ?
|
| 147 |
+
''', (STATUS_PENDING, limit))
|
| 148 |
+
|
| 149 |
+
accounts = []
|
| 150 |
+
for row in cursor.fetchall():
|
| 151 |
+
accounts.append({
|
| 152 |
+
'id': row[0],
|
| 153 |
+
'register_time': row[1],
|
| 154 |
+
'username': row[2],
|
| 155 |
+
'password': row[3],
|
| 156 |
+
'security_email': row[4],
|
| 157 |
+
'status': row[5],
|
| 158 |
+
'activation_link': row[6]
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
return accounts
|
| 162 |
+
|
| 163 |
+
def get_submitted_accounts(self, limit: int = 10) -> List[Dict[str, Any]]:
|
| 164 |
+
"""获取已提交但未提取链接的账号
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
limit: 返回账号的最大数量
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
账号列表,每个账号是一个字典
|
| 171 |
+
"""
|
| 172 |
+
with self.lock:
|
| 173 |
+
cursor = self.conn.cursor()
|
| 174 |
+
cursor.execute('''
|
| 175 |
+
SELECT id, register_time, username, password, security_email, status, activation_link
|
| 176 |
+
FROM accounts
|
| 177 |
+
WHERE status = ? AND (activation_link IS NULL OR activation_link = '')
|
| 178 |
+
LIMIT ?
|
| 179 |
+
''', (STATUS_SUBMITTED, limit))
|
| 180 |
+
|
| 181 |
+
accounts = []
|
| 182 |
+
for row in cursor.fetchall():
|
| 183 |
+
accounts.append({
|
| 184 |
+
'id': row[0],
|
| 185 |
+
'register_time': row[1],
|
| 186 |
+
'username': row[2],
|
| 187 |
+
'password': row[3],
|
| 188 |
+
'security_email': row[4],
|
| 189 |
+
'status': row[5],
|
| 190 |
+
'activation_link': row[6]
|
| 191 |
+
})
|
| 192 |
+
|
| 193 |
+
return accounts
|
| 194 |
+
|
| 195 |
+
def update_account_status(self, id: int, status: int, activation_link: str = None, notes: str = None) -> bool:
|
| 196 |
+
"""更新账号状态
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
id: 账号ID
|
| 200 |
+
status: 新状态
|
| 201 |
+
activation_link: 激活链接(可选)
|
| 202 |
+
notes: 备注信息(可选)
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
更新成功返回True,否则返回False
|
| 206 |
+
"""
|
| 207 |
+
try:
|
| 208 |
+
with self.lock:
|
| 209 |
+
cursor = self.conn.cursor()
|
| 210 |
+
|
| 211 |
+
if activation_link and notes:
|
| 212 |
+
cursor.execute('''
|
| 213 |
+
UPDATE accounts
|
| 214 |
+
SET status = ?, activation_link = ?, notes = ?, updated_at = datetime('now', 'localtime')
|
| 215 |
+
WHERE id = ?
|
| 216 |
+
''', (status, activation_link, notes, id))
|
| 217 |
+
elif activation_link:
|
| 218 |
+
cursor.execute('''
|
| 219 |
+
UPDATE accounts
|
| 220 |
+
SET status = ?, activation_link = ?, updated_at = datetime('now', 'localtime')
|
| 221 |
+
WHERE id = ?
|
| 222 |
+
''', (status, activation_link, id))
|
| 223 |
+
elif notes:
|
| 224 |
+
cursor.execute('''
|
| 225 |
+
UPDATE accounts
|
| 226 |
+
SET status = ?, notes = ?, updated_at = datetime('now', 'localtime')
|
| 227 |
+
WHERE id = ?
|
| 228 |
+
''', (status, notes, id))
|
| 229 |
+
else:
|
| 230 |
+
cursor.execute('''
|
| 231 |
+
UPDATE accounts
|
| 232 |
+
SET status = ?, updated_at = datetime('now', 'localtime')
|
| 233 |
+
WHERE id = ?
|
| 234 |
+
''', (status, id))
|
| 235 |
+
|
| 236 |
+
self.conn.commit()
|
| 237 |
+
return cursor.rowcount > 0
|
| 238 |
+
except Exception as e:
|
| 239 |
+
logger.error(f"更新账号状态时出错: {str(e)}")
|
| 240 |
+
return False
|
| 241 |
+
|
| 242 |
+
def log_operation(self, username: str, operation: str, status: str, message: str = None) -> None:
|
| 243 |
+
"""记录操作日志
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
username: 相关的用户名
|
| 247 |
+
operation: 操作类型
|
| 248 |
+
status: 操作状态
|
| 249 |
+
message: 附加信息
|
| 250 |
+
"""
|
| 251 |
+
try:
|
| 252 |
+
with self.lock:
|
| 253 |
+
cursor = self.conn.cursor()
|
| 254 |
+
cursor.execute('''
|
| 255 |
+
INSERT INTO operation_logs
|
| 256 |
+
(username, operation, status, message, created_at)
|
| 257 |
+
VALUES (?, ?, ?, ?, datetime('now', 'localtime'))
|
| 258 |
+
''', (username, operation, status, message))
|
| 259 |
+
self.conn.commit()
|
| 260 |
+
except Exception as e:
|
| 261 |
+
logger.error(f"记录操作日志时出错: {str(e)}")
|
| 262 |
+
|
| 263 |
+
def export_results_to_file(self, filepath: str, status: int = None) -> int:
|
| 264 |
+
"""将结果导出到文件
|
| 265 |
+
|
| 266 |
+
Args:
|
| 267 |
+
filepath: 输出文件路径
|
| 268 |
+
status: 筛选的状态(可选)
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
导出的记录数量
|
| 272 |
+
"""
|
| 273 |
+
try:
|
| 274 |
+
with self.lock, open(filepath, 'w', encoding='utf-8') as file:
|
| 275 |
+
cursor = self.conn.cursor()
|
| 276 |
+
|
| 277 |
+
if status is not None:
|
| 278 |
+
cursor.execute('''
|
| 279 |
+
SELECT register_time, username, password, security_email, status, activation_link
|
| 280 |
+
FROM accounts
|
| 281 |
+
WHERE status = ?
|
| 282 |
+
''', (status,))
|
| 283 |
+
else:
|
| 284 |
+
cursor.execute('''
|
| 285 |
+
SELECT register_time, username, password, security_email, status, activation_link
|
| 286 |
+
FROM accounts
|
| 287 |
+
''')
|
| 288 |
+
|
| 289 |
+
count = 0
|
| 290 |
+
for row in cursor.fetchall():
|
| 291 |
+
# 将状态码转换为文本
|
| 292 |
+
status_text = ""
|
| 293 |
+
if row[4] == STATUS_SUBMITTED:
|
| 294 |
+
status_text = "success"
|
| 295 |
+
elif row[4] == STATUS_FAILED:
|
| 296 |
+
status_text = "failed"
|
| 297 |
+
elif row[4] == STATUS_LINK_EXTRACTED:
|
| 298 |
+
status_text = "successed"
|
| 299 |
+
|
| 300 |
+
# 构建输出行
|
| 301 |
+
if row[5]: # 如果有激活链接
|
| 302 |
+
line = f"{row[0]}---{row[1]}---{row[2]}---{row[3]}---{status_text}---{row[5]}\n"
|
| 303 |
+
elif status_text:
|
| 304 |
+
line = f"{row[0]}---{row[1]}---{row[2]}---{row[3]}---{status_text}\n"
|
| 305 |
+
else:
|
| 306 |
+
line = f"{row[0]}---{row[1]}---{row[2]}---{row[3]}\n"
|
| 307 |
+
|
| 308 |
+
file.write(line)
|
| 309 |
+
count += 1
|
| 310 |
+
|
| 311 |
+
return count
|
| 312 |
+
except Exception as e:
|
| 313 |
+
logger.error(f"导出结果到文件时出错: {str(e)}")
|
| 314 |
+
return 0
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
class JetbrainsSubmitter:
|
| 318 |
+
"""提交邮箱到JetBrains获取激活链接的类"""
|
| 319 |
+
|
| 320 |
+
def __init__(self, db_manager: DatabaseManager, proxy: str = None):
|
| 321 |
+
"""初始化提交器
|
| 322 |
+
|
| 323 |
+
Args:
|
| 324 |
+
db_manager: 数据库管理器实例
|
| 325 |
+
proxy: 代理服务器地址
|
| 326 |
+
"""
|
| 327 |
+
self.db_manager = db_manager
|
| 328 |
+
self.proxy = proxy
|
| 329 |
+
logger.info("JetbrainsSubmitter 初始化完成")
|
| 330 |
+
|
| 331 |
+
def random_name(self) -> Tuple[str, str]:
|
| 332 |
+
"""生成随机的意大利名字和姓氏
|
| 333 |
+
|
| 334 |
+
Returns:
|
| 335 |
+
(名字, 姓氏)元组
|
| 336 |
+
"""
|
| 337 |
+
first_names = [
|
| 338 |
+
"Marco", "Giuseppe", "Antonio", "Giovanni", "Mario", "Luigi", "Paolo", "Francesco", "Roberto", "Stefano",
|
| 339 |
+
"Alessandro", "Andrea", "Giorgio", "Bruno", "Carlo", "Enrico", "Fabio", "Davide", "Claudio", "Massimo",
|
| 340 |
+
"Sofia", "Giulia", "Isabella", "Valentina", "Chiara", "Laura", "Maria", "Anna", "Francesca", "Elena",
|
| 341 |
+
"Alessandra", "Martina", "Giovanna", "Rosa", "Angela", "Lucia", "Paola", "Silvia", "Monica", "Cristina"
|
| 342 |
+
]
|
| 343 |
+
last_names = [
|
| 344 |
+
"Rossi", "Ferrari", "Russo", "Bianchi", "Romano", "Gallo", "Costa", "Fontana", "Conti", "Esposito",
|
| 345 |
+
"Ricci", "Bruno", "De Luca", "Moretti", "Marino", "Greco", "Barbieri", "Lombardi", "Giordano", "Colombo",
|
| 346 |
+
"Mancini", "Longo", "Leone", "Martinelli", "Santoro", "Mariani", "Vitale", "Ferraro", "Rinaldi", "Villa"
|
| 347 |
+
]
|
| 348 |
+
return random.choice(first_names), random.choice(last_names)
|
| 349 |
+
|
| 350 |
+
def submit_email(self, account: Dict[str, Any]) -> bool:
|
| 351 |
+
"""提交邮箱到JetBrains获取激活链接
|
| 352 |
+
|
| 353 |
+
Args:
|
| 354 |
+
account: 账号信息字典
|
| 355 |
+
|
| 356 |
+
Returns:
|
| 357 |
+
提交成功返回True,否则返回False
|
| 358 |
+
"""
|
| 359 |
+
username = account['username']
|
| 360 |
+
logger.info(f"正在尝试提交邮箱: {username} 到JetBrains...")
|
| 361 |
+
|
| 362 |
+
try:
|
| 363 |
+
firstname, lastname = self.random_name()
|
| 364 |
+
|
| 365 |
+
# 使用CaptchaSolver提交邮箱
|
| 366 |
+
with CaptchaSolver(
|
| 367 |
+
email=username,
|
| 368 |
+
firstname=firstname,
|
| 369 |
+
lastname=lastname,
|
| 370 |
+
is_teacher=False,
|
| 371 |
+
proxy=self.proxy
|
| 372 |
+
) as solver:
|
| 373 |
+
success = solver.solve_audio_captcha()
|
| 374 |
+
|
| 375 |
+
if success:
|
| 376 |
+
logger.info(f"邮箱 {username} 提交成功")
|
| 377 |
+
self.db_manager.update_account_status(account['id'], STATUS_SUBMITTED)
|
| 378 |
+
self.db_manager.log_operation(username, "submit_email", "success")
|
| 379 |
+
return True
|
| 380 |
+
else:
|
| 381 |
+
logger.warning(f"邮箱 {username} 提交失败 (CaptchaSolver返回False)")
|
| 382 |
+
self.db_manager.update_account_status(account['id'], STATUS_FAILED, notes="提交失败请手动检查")
|
| 383 |
+
self.db_manager.log_operation(username, "submit_email", "failed", "CaptchaSolver返回False")
|
| 384 |
+
return False
|
| 385 |
+
except Exception as e:
|
| 386 |
+
error_msg = str(e)
|
| 387 |
+
logger.error(f"提交邮箱 {username} 时发生错误: {error_msg}")
|
| 388 |
+
self.db_manager.update_account_status(account['id'], STATUS_FAILED, notes=f"认证失败: {error_msg[:100]}")
|
| 389 |
+
self.db_manager.log_operation(username, "submit_email", "error", error_msg[:200])
|
| 390 |
+
return False
|
| 391 |
+
|
| 392 |
+
def submit_batch(self, max_accounts: int = 5, max_workers: int = 3) -> Tuple[int, int]:
|
| 393 |
+
"""批量提交邮箱
|
| 394 |
+
|
| 395 |
+
Args:
|
| 396 |
+
max_accounts: 最大处理账号数量
|
| 397 |
+
max_workers: 最大并发线程数
|
| 398 |
+
|
| 399 |
+
Returns:
|
| 400 |
+
(成功数量, 失败数量)元组
|
| 401 |
+
"""
|
| 402 |
+
accounts = self.db_manager.get_pending_accounts(limit=max_accounts)
|
| 403 |
+
if not accounts:
|
| 404 |
+
logger.info("没有待处理的账号")
|
| 405 |
+
return 0, 0
|
| 406 |
+
|
| 407 |
+
success_count = 0
|
| 408 |
+
error_count = 0
|
| 409 |
+
logger.info(f"开始批量处理 {len(accounts)} 个账号,最大并发数: {max_workers}")
|
| 410 |
+
|
| 411 |
+
# 使用线程池并发处理账号
|
| 412 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 413 |
+
future_to_account = {executor.submit(self.submit_email, account): account for account in accounts}
|
| 414 |
+
|
| 415 |
+
for future in concurrent.futures.as_completed(future_to_account):
|
| 416 |
+
account = future_to_account[future]
|
| 417 |
+
try:
|
| 418 |
+
success = future.result()
|
| 419 |
+
if success:
|
| 420 |
+
success_count += 1
|
| 421 |
+
else:
|
| 422 |
+
error_count += 1
|
| 423 |
+
except Exception as e:
|
| 424 |
+
logger.error(f"执行任务时发生异常: {str(e)}")
|
| 425 |
+
error_count += 1
|
| 426 |
+
# 更新数据库状态
|
| 427 |
+
self.db_manager.update_account_status(account['id'], STATUS_FAILED, notes=f"执行异常: {str(e)[:100]}")
|
| 428 |
+
|
| 429 |
+
logger.info(f"批量处理完成,成功: {success_count},失败: {error_count}")
|
| 430 |
+
return success_count, error_count
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
class LinkExtractor:
|
| 434 |
+
"""从邮箱中提取JetBrains激活链接的类"""
|
| 435 |
+
|
| 436 |
+
def __init__(self, db_manager: DatabaseManager):
|
| 437 |
+
"""初始化链接提取器
|
| 438 |
+
|
| 439 |
+
Args:
|
| 440 |
+
db_manager: 数据库管理器实例
|
| 441 |
+
"""
|
| 442 |
+
self.db_manager = db_manager
|
| 443 |
+
logger.info("LinkExtractor 初始化完成")
|
| 444 |
+
|
| 445 |
+
def _find_activation_link(self, text_content: str) -> Optional[str]:
|
| 446 |
+
"""在文本内容中查找JetBrains激活链接
|
| 447 |
+
|
| 448 |
+
Args:
|
| 449 |
+
text_content: 邮件文本内容
|
| 450 |
+
|
| 451 |
+
Returns:
|
| 452 |
+
找到的激活链接,未找到返回None
|
| 453 |
+
"""
|
| 454 |
+
if not text_content:
|
| 455 |
+
return None
|
| 456 |
+
|
| 457 |
+
# 使用正则表达式查找JetBrains激活链接
|
| 458 |
+
match = re.search(r'https?://(?:account\.jetbrains\.com/login|www\.jetbrains\.com/shop/(?:account|eform)|jetbrains\.com/activate)[?\S]+', text_content)
|
| 459 |
+
if match:
|
| 460 |
+
link = match.group(0)
|
| 461 |
+
# 基本验证:确认是有效的JetBrains激活链接
|
| 462 |
+
if "jetbrains.com" in link and ("activate" in link or "account" in link or "login" in link or "eform" in link):
|
| 463 |
+
# 清理链接:移除可能的尾随字符
|
| 464 |
+
link = link.split('<')[0].split('>')[0].split('"')[0].split("'")[0].strip()
|
| 465 |
+
logger.info(f"找到可能的激活链接: {link}")
|
| 466 |
+
return link
|
| 467 |
+
return None
|
| 468 |
+
|
| 469 |
+
def extract_link(self, account: Dict[str, Any]) -> Optional[str]:
|
| 470 |
+
"""登录邮箱并提取JetBrains激活链接
|
| 471 |
+
|
| 472 |
+
Args:
|
| 473 |
+
account: 账号信息字典
|
| 474 |
+
|
| 475 |
+
Returns:
|
| 476 |
+
提取到的激活链接,未找到返回None
|
| 477 |
+
"""
|
| 478 |
+
username = account['username']
|
| 479 |
+
password = account['password']
|
| 480 |
+
logger.info(f"正在尝试登录邮箱: {username} 并提取激活链接...")
|
| 481 |
+
|
| 482 |
+
try:
|
| 483 |
+
# 创建邮件客户端实例
|
| 484 |
+
client = EmailClient(
|
| 485 |
+
username=username,
|
| 486 |
+
password=password,
|
| 487 |
+
email_address=username
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
# 认证
|
| 491 |
+
logger.info(f"为 {username} 执行OAuth认证...")
|
| 492 |
+
client.authenticate_oauth()
|
| 493 |
+
logger.info(f"为 {username} OAuth认证成功")
|
| 494 |
+
|
| 495 |
+
# 搜索邮件
|
| 496 |
+
logger.info(f"为 {username} 搜索JetBrains相关邮件...")
|
| 497 |
+
search_keywords = [
|
| 498 |
+
'BODY "jetbrain"',
|
| 499 |
+
'SUBJECT "JetBrains Account"',
|
| 500 |
+
'SUBJECT "Activate JetBrains"',
|
| 501 |
+
'BODY "jetbrains.com/activate"',
|
| 502 |
+
'BODY "account.jetbrains.com"',
|
| 503 |
+
'FROM "jetbrains.com"'
|
| 504 |
+
]
|
| 505 |
+
|
| 506 |
+
emails = []
|
| 507 |
+
for keyword in search_keywords:
|
| 508 |
+
logger.info(f"使用关键词 '{keyword}' 搜索...")
|
| 509 |
+
try:
|
| 510 |
+
fetched_emails = client.read_emails(mailbox="INBOX", limit=5, keyword=keyword)
|
| 511 |
+
emails.extend(fetched_emails)
|
| 512 |
+
if emails: # 找到邮件后停止搜索
|
| 513 |
+
logger.info(f"使用关键词 '{keyword}' 找到 {len(fetched_emails)} 封邮件")
|
| 514 |
+
break
|
| 515 |
+
except Exception as read_err:
|
| 516 |
+
logger.warning(f"使用关键词 '{keyword}' 读取邮件时出错: {read_err}")
|
| 517 |
+
time.sleep(1) # 每次搜索之间短暂延迟
|
| 518 |
+
|
| 519 |
+
if not emails:
|
| 520 |
+
logger.warning(f"邮箱 {username} 中未找到相关的JetBrains邮件")
|
| 521 |
+
self.db_manager.update_account_status(account['id'], STATUS_SUBMITTED, notes="没有找到包含关键字的邮件")
|
| 522 |
+
self.db_manager.log_operation(username, "extract_link", "failed", "没有找到包含关键字的邮件")
|
| 523 |
+
return None
|
| 524 |
+
|
| 525 |
+
logger.info(f"在 {username} 中找到 {len(emails)} 封相关邮件,开始解析...")
|
| 526 |
+
|
| 527 |
+
# 解析邮件内容
|
| 528 |
+
for email_content in emails:
|
| 529 |
+
html_body = email_content.get('html')
|
| 530 |
+
text_body = email_content.get('text')
|
| 531 |
+
|
| 532 |
+
link = None
|
| 533 |
+
if html_body:
|
| 534 |
+
link = self._find_activation_link(html_body)
|
| 535 |
+
if not link and text_body:
|
| 536 |
+
link = self._find_activation_link(text_body)
|
| 537 |
+
|
| 538 |
+
if link:
|
| 539 |
+
logger.success(f"为邮箱 {username} 成功提取到激活链接: {link}")
|
| 540 |
+
self.db_manager.update_account_status(account['id'], STATUS_LINK_EXTRACTED, activation_link=link)
|
| 541 |
+
self.db_manager.log_operation(username, "extract_link", "success", link)
|
| 542 |
+
return link
|
| 543 |
+
|
| 544 |
+
logger.warning(f"在找到的邮件中未能为邮箱 {username} 提取到激活链接")
|
| 545 |
+
self.db_manager.update_account_status(account['id'], STATUS_SUBMITTED, notes="没有找到激活链接")
|
| 546 |
+
self.db_manager.log_operation(username, "extract_link", "failed", "在邮件中未找到链接")
|
| 547 |
+
return None
|
| 548 |
+
|
| 549 |
+
except Exception as e:
|
| 550 |
+
error_msg = str(e)
|
| 551 |
+
# 捕获特定认证错误
|
| 552 |
+
if "Authentication failed" in error_msg or "认证失败" in error_msg:
|
| 553 |
+
logger.error(f"邮箱 {username} 认证失败: {error_msg}")
|
| 554 |
+
error_type = "认证失败"
|
| 555 |
+
elif "invalid_grant" in error_msg:
|
| 556 |
+
logger.error(f"邮箱 {username} OAuth Token无效或过期: {error_msg}")
|
| 557 |
+
error_type = "OAuth Token无效"
|
| 558 |
+
else:
|
| 559 |
+
logger.error(f"为邮箱 {username} 提取链接时发生未预料的错误: {error_msg}")
|
| 560 |
+
error_type = "未知错误"
|
| 561 |
+
|
| 562 |
+
self.db_manager.update_account_status(account['id'], STATUS_SUBMITTED, notes=f"{error_type}: {error_msg[:100]}")
|
| 563 |
+
self.db_manager.log_operation(username, "extract_link", "error", error_msg[:200])
|
| 564 |
+
return None
|
| 565 |
+
|
| 566 |
+
def extract_batch(self, max_accounts: int = 5, max_workers: int = 1) -> Tuple[int, int]:
|
| 567 |
+
"""批量提取激活链接
|
| 568 |
+
|
| 569 |
+
Args:
|
| 570 |
+
max_accounts: 最大处理账号数量
|
| 571 |
+
max_workers: 最大并发线程数
|
| 572 |
+
|
| 573 |
+
Returns:
|
| 574 |
+
(成功数量, 失败数量)元组
|
| 575 |
+
"""
|
| 576 |
+
accounts = self.db_manager.get_submitted_accounts(limit=max_accounts)
|
| 577 |
+
if not accounts:
|
| 578 |
+
logger.info("没有待提取链接的账号")
|
| 579 |
+
return 0, 0
|
| 580 |
+
|
| 581 |
+
success_count = 0
|
| 582 |
+
error_count = 0
|
| 583 |
+
logger.info(f"开始批量提取链接,处理 {len(accounts)} 个账号,最大并发数: {max_workers}")
|
| 584 |
+
|
| 585 |
+
# 使用线程池并发处理账号
|
| 586 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 587 |
+
future_to_account = {executor.submit(self.extract_link, account): account for account in accounts}
|
| 588 |
+
|
| 589 |
+
for future in concurrent.futures.as_completed(future_to_account):
|
| 590 |
+
account = future_to_account[future]
|
| 591 |
+
try:
|
| 592 |
+
link = future.result()
|
| 593 |
+
if link:
|
| 594 |
+
success_count += 1
|
| 595 |
+
else:
|
| 596 |
+
error_count += 1
|
| 597 |
+
except Exception as e:
|
| 598 |
+
logger.error(f"执行提取任务时发生异常: {str(e)}")
|
| 599 |
+
error_count += 1
|
| 600 |
+
# 更新数据库状态
|
| 601 |
+
self.db_manager.update_account_status(account['id'], STATUS_SUBMITTED, notes=f"执行异常: {str(e)[:100]}")
|
| 602 |
+
|
| 603 |
+
logger.info(f"批量提取完成,成功: {success_count},失败: {error_count}")
|
| 604 |
+
return success_count, error_count
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
class ProcessController:
|
| 608 |
+
"""流程控制类,协调整个处理流程"""
|
| 609 |
+
|
| 610 |
+
def __init__(self, db_path: str = "unibo_jetbrains.db", proxy: str = ''):
|
| 611 |
+
"""初始化流程控制器
|
| 612 |
+
|
| 613 |
+
Args:
|
| 614 |
+
db_path: 数据库文件路径
|
| 615 |
+
proxy: 代理服务器地址
|
| 616 |
+
"""
|
| 617 |
+
self.db_manager = DatabaseManager(db_path)
|
| 618 |
+
self.submitter = JetbrainsSubmitter(self.db_manager, proxy)
|
| 619 |
+
self.extractor = LinkExtractor(self.db_manager)
|
| 620 |
+
logger.info("ProcessController 初始化完成")
|
| 621 |
+
|
| 622 |
+
def import_data(self, filepath: str) -> int:
|
| 623 |
+
"""从文件导入账号数据
|
| 624 |
+
|
| 625 |
+
Args:
|
| 626 |
+
filepath: 账号数据文件路径
|
| 627 |
+
|
| 628 |
+
Returns:
|
| 629 |
+
导入的账号数量
|
| 630 |
+
"""
|
| 631 |
+
return self.db_manager.import_from_file(filepath)
|
| 632 |
+
|
| 633 |
+
def export_data(self, filepath: str, status: int = None) -> int:
|
| 634 |
+
"""导出账号数据到文件
|
| 635 |
+
|
| 636 |
+
Args:
|
| 637 |
+
filepath: 输出文件路径
|
| 638 |
+
status: 筛选状态(可选)
|
| 639 |
+
|
| 640 |
+
Returns:
|
| 641 |
+
导出的记录数量
|
| 642 |
+
"""
|
| 643 |
+
return self.db_manager.export_results_to_file(filepath, status)
|
| 644 |
+
|
| 645 |
+
def run_submission_process(self, max_accounts: int = 5, max_workers: int = 3) -> Tuple[int, int]:
|
| 646 |
+
"""运行邮箱提交流程
|
| 647 |
+
|
| 648 |
+
Args:
|
| 649 |
+
max_accounts: 最大处理账号数量
|
| 650 |
+
max_workers: 最大并发线程数
|
| 651 |
+
|
| 652 |
+
Returns:
|
| 653 |
+
(成功数量, 失败数量)元组
|
| 654 |
+
"""
|
| 655 |
+
return self.submitter.submit_batch(max_accounts, max_workers)
|
| 656 |
+
|
| 657 |
+
def run_extraction_process(self, max_accounts: int = 5, max_workers: int = 1) -> Tuple[int, int]:
|
| 658 |
+
"""运行链接提取流程
|
| 659 |
+
|
| 660 |
+
Args:
|
| 661 |
+
max_accounts: 最大处理账号数量
|
| 662 |
+
max_workers: 最大并发线程数
|
| 663 |
+
|
| 664 |
+
Returns:
|
| 665 |
+
(成功数量, 失败数量)元组
|
| 666 |
+
"""
|
| 667 |
+
return self.extractor.extract_batch(max_accounts, max_workers)
|
| 668 |
+
|
| 669 |
+
def run_full_process(self, max_submission: int = 5, max_extraction: int = 5,
|
| 670 |
+
submission_workers: int = 3, extraction_workers: int = 1) -> Dict[str, int]:
|
| 671 |
+
"""运行完整流程:提交邮箱并提取链接
|
| 672 |
+
|
| 673 |
+
Args:
|
| 674 |
+
max_submission: 提交流程最大处理账号数量
|
| 675 |
+
max_extraction: 提取流程最大处理账号数量
|
| 676 |
+
submission_workers: 提交流程最大并发线程数
|
| 677 |
+
extraction_workers: 提取流程最大并发线程数
|
| 678 |
+
|
| 679 |
+
Returns:
|
| 680 |
+
包含各步骤统计信息的字典
|
| 681 |
+
"""
|
| 682 |
+
# 运行提交流程
|
| 683 |
+
sub_success, sub_fail = self.run_submission_process(max_submission, submission_workers)
|
| 684 |
+
|
| 685 |
+
# 运行提取流程
|
| 686 |
+
ext_success, ext_fail = self.run_extraction_process(max_extraction, extraction_workers)
|
| 687 |
+
|
| 688 |
+
return {
|
| 689 |
+
'submission_success': sub_success,
|
| 690 |
+
'submission_fail': sub_fail,
|
| 691 |
+
'extraction_success': ext_success,
|
| 692 |
+
'extraction_fail': ext_fail,
|
| 693 |
+
'total_processed': sub_success + sub_fail + ext_success + ext_fail
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
|
| 697 |
+
# 示例用法
|
| 698 |
+
if __name__ == "__main__":
|
| 699 |
+
# 设置日志级别
|
| 700 |
+
logger.remove()
|
| 701 |
+
logger.add(lambda msg: print(msg, end=""), level="INFO")
|
| 702 |
+
logger.add("unibo_process.log", rotation="500 KB", level="DEBUG")
|
| 703 |
+
|
| 704 |
+
# 初始化流程控制器
|
| 705 |
+
controller = ProcessController()
|
| 706 |
+
|
| 707 |
+
# 导入数据
|
| 708 |
+
print("\n=== 从文件导入数据 ===")
|
| 709 |
+
imported = controller.import_data("user-4-21-success.txt")
|
| 710 |
+
print(f"导入了 {imported} 个账号")
|
| 711 |
+
|
| 712 |
+
# 运行提交流程
|
| 713 |
+
print("\n=== 运行提交邮箱流程 ===")
|
| 714 |
+
sub_success, sub_fail = controller.run_submission_process(max_accounts=3, max_workers=1)
|
| 715 |
+
print(f"提交结果: 成功 {sub_success} 个, 失败 {sub_fail} 个")
|
| 716 |
+
|
| 717 |
+
# 运行提取链接流程
|
| 718 |
+
print("\n=== 运行提取链接流程 ===")
|
| 719 |
+
ext_success, ext_fail = controller.run_extraction_process(max_accounts=2, max_workers=1)
|
| 720 |
+
print(f"提取结果: 成功 {ext_success} 个, 失败 {ext_fail} 个")
|
| 721 |
+
|
| 722 |
+
# 导出结果
|
| 723 |
+
print("\n=== 导出链接提取成功的账号 ===")
|
| 724 |
+
exported = controller.export_data("export_results.txt", STATUS_LINK_EXTRACTED)
|
| 725 |
+
print(f"导出了 {exported} 个记录")
|
收发邮件.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import email
|
| 3 |
+
import imaplib
|
| 4 |
+
import smtplib
|
| 5 |
+
import requests
|
| 6 |
+
from email.header import decode_header, Header
|
| 7 |
+
from email.mime.text import MIMEText
|
| 8 |
+
from email.mime.multipart import MIMEMultipart
|
| 9 |
+
from typing import List, Dict
|
| 10 |
+
from unibo_auth2_get_AuthCode_RefreshToken_sucsses import OAuth2Authenticator
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class EmailClient:
|
| 14 |
+
def __init__(self, username: str, password: str, email_address: str):
|
| 15 |
+
"""
|
| 16 |
+
Initialize the EmailClient with user credentials.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
username: The username for authentication
|
| 20 |
+
password: The password for authentication
|
| 21 |
+
email_address: The full email address
|
| 22 |
+
"""
|
| 23 |
+
self.username = username
|
| 24 |
+
self.password = password
|
| 25 |
+
self.email_address = email_address
|
| 26 |
+
self.client_id = "9e5f94bc-e8a4-4e73-b8be-63364c29d753"
|
| 27 |
+
self.imap_server = "outlook.office365.com"
|
| 28 |
+
self.imap_port = 993
|
| 29 |
+
# self.smtp_server = "submission.unipi.it" # 使用密码登录
|
| 30 |
+
self.smtp_server_XOAUTH2 = "smtp.office365.com" # 使用OAuth2登录
|
| 31 |
+
self.smtp_port = 587
|
| 32 |
+
self.refresh_token = None
|
| 33 |
+
self.access_token = None
|
| 34 |
+
|
| 35 |
+
def authenticate_oauth(self) -> None:
|
| 36 |
+
"""Authenticate using OAuth2 and get refresh/access tokens."""
|
| 37 |
+
authenticator = OAuth2Authenticator(
|
| 38 |
+
username=self.username,
|
| 39 |
+
password=self.password
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
auth_code = authenticator.execute_flow()
|
| 44 |
+
print(f"成功获取授权码: {auth_code}")
|
| 45 |
+
self.refresh_token, self.access_token = authenticator.get_refresh_token(auth_code)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
raise Exception(f"认证失败: {e}")
|
| 48 |
+
|
| 49 |
+
def _generate_auth_string(self) -> bytes:
|
| 50 |
+
"""Generate XOAUTH2 authentication string."""
|
| 51 |
+
auth_string = f"user={self.email_address}\x01auth=Bearer {self.access_token}\x01\x01"
|
| 52 |
+
return auth_string.encode('utf-8')
|
| 53 |
+
|
| 54 |
+
def connect_imap(self) -> imaplib.IMAP4:
|
| 55 |
+
"""
|
| 56 |
+
Connect to IMAP server using XOAUTH2 authentication.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
IMAP4 connection object
|
| 60 |
+
|
| 61 |
+
Raises:
|
| 62 |
+
Exception: If authentication fails
|
| 63 |
+
"""
|
| 64 |
+
try:
|
| 65 |
+
imap = imaplib.IMAP4_SSL(self.imap_server, self.imap_port)
|
| 66 |
+
|
| 67 |
+
if 'AUTH=XOAUTH2' not in imap.capabilities:
|
| 68 |
+
raise Exception("Server does not support XOAUTH2 authentication")
|
| 69 |
+
|
| 70 |
+
auth_bytes = self._generate_auth_string()
|
| 71 |
+
|
| 72 |
+
def oauth2_callback(response):
|
| 73 |
+
return auth_bytes
|
| 74 |
+
|
| 75 |
+
print(f"Attempting XOAUTH2 authentication as {self.email_address}...")
|
| 76 |
+
typ, data = imap.authenticate('XOAUTH2', oauth2_callback)
|
| 77 |
+
if typ != 'OK':
|
| 78 |
+
raise Exception(f"Authentication failed: {data[0].decode('utf-8', errors='replace')}")
|
| 79 |
+
|
| 80 |
+
print("XOAUTH2 authentication successful")
|
| 81 |
+
return imap
|
| 82 |
+
except Exception as e:
|
| 83 |
+
raise Exception(f"IMAP XOAUTH2 authentication failed: {str(e)}")
|
| 84 |
+
|
| 85 |
+
def _get_decoded_header(self, header: str) -> str:
|
| 86 |
+
"""解码邮件头信息
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
header: 需要解码的邮件头字符串
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
解码后的字符串
|
| 93 |
+
"""
|
| 94 |
+
try:
|
| 95 |
+
decoded = decode_header(header)
|
| 96 |
+
|
| 97 |
+
# 修复括号匹配问题
|
| 98 |
+
return "".join(
|
| 99 |
+
str(
|
| 100 |
+
t[0].decode(t[1] or "utf-8", errors="replace")
|
| 101 |
+
if isinstance(t[0], bytes)
|
| 102 |
+
else t[0]
|
| 103 |
+
)
|
| 104 |
+
for t in decoded
|
| 105 |
+
)
|
| 106 |
+
except Exception as e:
|
| 107 |
+
print(f"邮件头解码失败: {str(e)}")
|
| 108 |
+
return header
|
| 109 |
+
|
| 110 |
+
def _parse_email_message(self, msg: email.message.Message) -> Dict:
|
| 111 |
+
"""Parse an email message into a dictionary."""
|
| 112 |
+
email_data = {}
|
| 113 |
+
|
| 114 |
+
# Parse headers
|
| 115 |
+
email_data["from"] = self._get_decoded_header(msg["from"]) if msg["from"] else "N/A"
|
| 116 |
+
email_data["subject"] = self._get_decoded_header(msg["subject"]) if msg["subject"] else "N/A"
|
| 117 |
+
email_data["date"] = msg["date"] if msg["date"] else "N/A"
|
| 118 |
+
email_data["to"] = self._get_decoded_header(msg.get("to", ""))
|
| 119 |
+
|
| 120 |
+
# Parse body content
|
| 121 |
+
def get_body_content(part):
|
| 122 |
+
charset = part.get_content_charset() or "utf-8"
|
| 123 |
+
try:
|
| 124 |
+
return part.get_payload(decode=True).decode(charset, errors="replace")
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f"正文解码失败: {str(e)}")
|
| 127 |
+
return ""
|
| 128 |
+
|
| 129 |
+
if msg.is_multipart():
|
| 130 |
+
for part in msg.walk():
|
| 131 |
+
content_type = part.get_content_type()
|
| 132 |
+
content_disposition = str(part.get("Content-Disposition"))
|
| 133 |
+
if "attachment" in content_disposition:
|
| 134 |
+
continue
|
| 135 |
+
if content_type == "text/plain":
|
| 136 |
+
email_data["text"] = get_body_content(part)
|
| 137 |
+
elif content_type == "text/html":
|
| 138 |
+
email_data["html"] = get_body_content(part)
|
| 139 |
+
else:
|
| 140 |
+
content_type = msg.get_content_type()
|
| 141 |
+
if content_type == "text/plain":
|
| 142 |
+
email_data["text"] = get_body_content(msg)
|
| 143 |
+
elif content_type == "text/html":
|
| 144 |
+
email_data["html"] = get_body_content(msg)
|
| 145 |
+
|
| 146 |
+
return email_data
|
| 147 |
+
|
| 148 |
+
def read_emails(self, mailbox: str = "INBOX", limit: int = 1, keyword: str = 'ALL') -> List[Dict]:
|
| 149 |
+
"""
|
| 150 |
+
Read emails from the specified mailbox.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
mailbox: Mailbox to read from (default: INBOX)
|
| 154 |
+
limit: Maximum number of emails to return (default: 1)
|
| 155 |
+
keyword: Search keyword (default: 'ALL')
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
List of parsed email dictionaries
|
| 159 |
+
"""
|
| 160 |
+
imap = self.connect_imap()
|
| 161 |
+
try:
|
| 162 |
+
status, _ = imap.select(mailbox, readonly=True)
|
| 163 |
+
if status != 'OK':
|
| 164 |
+
raise Exception(f"无法选择邮箱 {mailbox}")
|
| 165 |
+
|
| 166 |
+
_, messages = imap.search(None, keyword)
|
| 167 |
+
email_list = []
|
| 168 |
+
message_ids = messages[0].split()
|
| 169 |
+
|
| 170 |
+
if not message_ids:
|
| 171 |
+
print(f"邮箱 {mailbox} 中没有邮件")
|
| 172 |
+
return []
|
| 173 |
+
|
| 174 |
+
for num in reversed(message_ids[:limit]):
|
| 175 |
+
_, msg_data = imap.fetch(num, "(RFC822)")
|
| 176 |
+
email_message = email.message_from_bytes(msg_data[0][1])
|
| 177 |
+
email_data = self._parse_email_message(email_message)
|
| 178 |
+
email_data["uid"] = num.decode()
|
| 179 |
+
email_list.append(email_data)
|
| 180 |
+
|
| 181 |
+
print(f"成功读取 {len(email_list)} 封邮件")
|
| 182 |
+
return email_list
|
| 183 |
+
finally:
|
| 184 |
+
imap.logout()
|
| 185 |
+
|
| 186 |
+
def send_email(self, receiver: str, subject: str, body: str = None, is_html: bool = True) -> None:
|
| 187 |
+
"""
|
| 188 |
+
Send an email using SMTP.
|
| 189 |
+
|
| 190 |
+
Args:
|
| 191 |
+
receiver: Recipient email address
|
| 192 |
+
subject: Email subject
|
| 193 |
+
body: Email body content
|
| 194 |
+
is_html: Whether the body is HTML (default: True)
|
| 195 |
+
|
| 196 |
+
Raises:
|
| 197 |
+
Exception: If email sending fails
|
| 198 |
+
"""
|
| 199 |
+
if body is None:
|
| 200 |
+
body = """
|
| 201 |
+
<h1>Python SMTP 测试邮件</h1>
|
| 202 |
+
<p>这是一封通过Python SMTP发送的测试邮件。</p>
|
| 203 |
+
"""
|
| 204 |
+
is_html = True
|
| 205 |
+
|
| 206 |
+
message = MIMEMultipart()
|
| 207 |
+
# message['From'] = self.email_address
|
| 208 |
+
message['To'] = receiver
|
| 209 |
+
message['Subject'] = subject
|
| 210 |
+
|
| 211 |
+
message.attach(MIMEText(body, 'html' if is_html else 'plain', 'utf-8'))
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
if self.smtp_port == 465:
|
| 215 |
+
server = smtplib.SMTP_SSL(self.smtp_server_XOAUTH2, self.smtp_port)
|
| 216 |
+
else:
|
| 217 |
+
server = smtplib.SMTP(self.smtp_server_XOAUTH2, self.smtp_port)
|
| 218 |
+
server.starttls()
|
| 219 |
+
|
| 220 |
+
# 建立 SMTP 连接
|
| 221 |
+
server = smtplib.SMTP(self.smtp_server_XOAUTH2, self.smtp_port)
|
| 222 |
+
# server.set_debuglevel(1) # 启用调试输出
|
| 223 |
+
server.ehlo() # 识别客户端
|
| 224 |
+
server.starttls() # 启用TLS加密
|
| 225 |
+
server.ehlo() # 再次识别客户端
|
| 226 |
+
# 准备OAuth2认证字符串
|
| 227 |
+
auth_string = f"user={self.email_address}\1auth=Bearer {self.access_token}\1\1"
|
| 228 |
+
auth_string = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8')
|
| 229 |
+
|
| 230 |
+
# 执行OAuth2认证
|
| 231 |
+
code, response = server.docmd('AUTH', 'XOAUTH2 ' + auth_string)
|
| 232 |
+
server.sendmail(self.email_address, [receiver], message.as_string())
|
| 233 |
+
print("邮件发送成功")
|
| 234 |
+
except Exception as e:
|
| 235 |
+
raise Exception(f"邮件发送失败: {e}")
|
| 236 |
+
finally:
|
| 237 |
+
server.quit()
|
| 238 |
+
|
| 239 |
+
def refresh_access_token(self) -> str:
|
| 240 |
+
"""
|
| 241 |
+
Refresh the access token using the refresh token.
|
| 242 |
+
|
| 243 |
+
Returns:
|
| 244 |
+
New access token
|
| 245 |
+
|
| 246 |
+
Raises:
|
| 247 |
+
Exception: If token refresh fails
|
| 248 |
+
"""
|
| 249 |
+
if not self.refresh_token:
|
| 250 |
+
raise Exception("No refresh token available")
|
| 251 |
+
|
| 252 |
+
url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
| 253 |
+
headers = {
|
| 254 |
+
"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Thunderbird/137.0',
|
| 255 |
+
"Content-Type": 'application/x-www-form-urlencoded;charset=UTF-8',
|
| 256 |
+
}
|
| 257 |
+
data = {
|
| 258 |
+
"client_id": self.client_id,
|
| 259 |
+
"grant_type": "refresh_token",
|
| 260 |
+
"refresh_token": self.refresh_token,
|
| 261 |
+
"scope": "https://outlook.office.com/IMAP.AccessAsUser.All offline_access",
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
response = requests.post(url, headers=headers, data=data, verify=False)
|
| 265 |
+
if response.status_code != 200:
|
| 266 |
+
error_text = response.text
|
| 267 |
+
if "invalid_grant" in error_text and "70000" in error_text:
|
| 268 |
+
print("\n[安全警告] 账户需要安全验证!")
|
| 269 |
+
print("微软检测到您的账户可能存在安全风险,需要进行额外验证。")
|
| 270 |
+
print("请登录 https://account.microsoft.com 或 https://account.live.com 进行安全验证")
|
| 271 |
+
print("验证完成后,您需要重新获取refreshtoken并更新Excel文件\n")
|
| 272 |
+
raise Exception(f"获取access token失败: {error_text}")
|
| 273 |
+
|
| 274 |
+
response_data = response.json()
|
| 275 |
+
self.access_token = response_data["access_token"]
|
| 276 |
+
if "refresh_token" in response_data:
|
| 277 |
+
self.refresh_token = response_data["refresh_token"]
|
| 278 |
+
|
| 279 |
+
return self.access_token
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
if __name__ == "__main__":
|
| 283 |
+
try:
|
| 284 |
+
# Example usage
|
| 285 |
+
client = EmailClient(
|
| 286 |
+
username="[email protected]",
|
| 287 |
+
password="123",
|
| 288 |
+
email_address="[email protected]"
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
# Authenticate and get tokens
|
| 292 |
+
client.authenticate_oauth()
|
| 293 |
+
#
|
| 294 |
+
# Read emails with GitHub keyword
|
| 295 |
+
emails = client.read_emails(mailbox="INBOX",
|
| 296 |
+
limit=5,keyword='BODY "jetbrain"')
|
| 297 |
+
|
| 298 |
+
# Print email details
|
| 299 |
+
for i, email_data in enumerate(emails, 1):
|
| 300 |
+
print(f"\n邮件 {i}:")
|
| 301 |
+
print(f"发件人: {email_data.get('from', 'N/A')}")
|
| 302 |
+
print(f"主题: {email_data.get('subject', 'N/A')}")
|
| 303 |
+
print(f"日期: {email_data.get('date', 'N/A')}")
|
| 304 |
+
print(f"文本内容: {email_data.get('text', 'N/A')[:1000]}...")
|
| 305 |
+
print("-" * 50)
|
| 306 |
+
|
| 307 |
+
# Send a test email
|
| 308 |
+
# client.send_email(
|
| 309 |
+
# receiver="[email protected]",
|
| 310 |
+
# subject="Test Email",
|
| 311 |
+
# body="This is a test email"
|
| 312 |
+
# )
|
| 313 |
+
|
| 314 |
+
except Exception as e:
|
| 315 |
+
print(f"Error: {str(e)}")
|
| 316 |
+
|