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