Delete api_server.py
Browse files- api_server.py +0 -915
api_server.py
DELETED
@@ -1,915 +0,0 @@
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|