BG5 commited on
Commit
f9cbb48
·
verified ·
1 Parent(s): 54a5977

Upload 6 files

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