diff --git a/requirements.txt b/requirements.txt index c6a9327..b2e6f4a 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index 71d14d0..a8f6db3 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -112,6 +112,44 @@ class TaskListResponse(BaseModel): tasks: List[RegistrationTaskResponse] +# ============== Outlook 批量注册模型 ============== + +class OutlookAccountForRegistration(BaseModel): + """可用于注册的 Outlook 账户""" + id: int # EmailService 表的 ID + email: str + name: str + has_oauth: bool # 是否有 OAuth 配置 + is_registered: bool # 是否已注册 + registered_account_id: Optional[int] = None + + +class OutlookAccountsListResponse(BaseModel): + """Outlook 账户列表响应""" + total: int + registered_count: int # 已注册数量 + unregistered_count: int # 未注册数量 + accounts: List[OutlookAccountForRegistration] + + +class OutlookBatchRegistrationRequest(BaseModel): + """Outlook 批量注册请求""" + service_ids: List[int] # 选中的 EmailService ID + skip_registered: bool = True # 自动跳过已注册邮箱 + proxy: Optional[str] = None + interval_min: int = 5 + interval_max: int = 30 + + +class OutlookBatchRegistrationResponse(BaseModel): + """Outlook 批量注册响应""" + batch_id: str + total: int # 总数 + skipped: int # 跳过数(已注册) + to_register: int # 待注册数 + service_ids: List[int] # 实际要注册的服务 ID + + # ============== Helper Functions ============== def task_to_response(task: RegistrationTask) -> RegistrationTaskResponse: @@ -688,3 +726,285 @@ async def get_available_email_services(): }) return result + + +# ============== Outlook 批量注册 API ============== + +@router.get("/outlook-accounts", response_model=OutlookAccountsListResponse) +async def get_outlook_accounts_for_registration(): + """ + 获取可用于注册的 Outlook 账户列表 + + 返回所有已启用的 Outlook 服务,并检查每个邮箱是否已在 accounts 表中注册 + """ + from ...database.models import EmailService as EmailServiceModel + from ...database.models import Account + + with get_db() as db: + # 获取所有启用的 Outlook 服务 + outlook_services = db.query(EmailServiceModel).filter( + EmailServiceModel.service_type == "outlook", + EmailServiceModel.enabled == True + ).order_by(EmailServiceModel.priority.asc()).all() + + accounts = [] + registered_count = 0 + unregistered_count = 0 + + for service in outlook_services: + config = service.config or {} + email = config.get("email") or service.name + + # 检查是否已注册(查询 accounts 表) + existing_account = db.query(Account).filter( + Account.email == email + ).first() + + is_registered = existing_account is not None + if is_registered: + registered_count += 1 + else: + unregistered_count += 1 + + accounts.append(OutlookAccountForRegistration( + id=service.id, + email=email, + name=service.name, + has_oauth=bool(config.get("client_id") and config.get("refresh_token")), + is_registered=is_registered, + registered_account_id=existing_account.id if existing_account else None + )) + + return OutlookAccountsListResponse( + total=len(accounts), + registered_count=registered_count, + unregistered_count=unregistered_count, + accounts=accounts + ) + + +async def run_outlook_batch_registration( + batch_id: str, + service_ids: List[int], + skip_registered: bool, + proxy: Optional[str], + interval_min: int, + interval_max: int +): + """ + 异步执行 Outlook 批量注册任务 + + 遍历选中的 Outlook 服务,检查邮箱是否已注册,执行注册任务 + """ + from ...database.models import EmailService as EmailServiceModel + from ...database.models import Account + + batch_tasks[batch_id] = { + "total": len(service_ids), + "completed": 0, + "success": 0, + "failed": 0, + "skipped": 0, + "cancelled": False, + "service_ids": service_ids, + "current_index": 0, + "logs": [] + } + + try: + for i, service_id in enumerate(service_ids): + # 检查是否已取消 + if batch_tasks[batch_id]["cancelled"]: + logger.info(f"Outlook 批量任务 {batch_id} 已取消") + break + + batch_tasks[batch_id]["current_index"] = i + + with get_db() as db: + # 获取邮箱服务 + service = db.query(EmailServiceModel).filter( + EmailServiceModel.id == service_id + ).first() + + if not service: + batch_tasks[batch_id]["logs"].append(f"[跳过] 服务 ID {service_id} 不存在") + batch_tasks[batch_id]["skipped"] += 1 + batch_tasks[batch_id]["completed"] += 1 + continue + + config = service.config or {} + email = config.get("email") or service.name + + # 检查是否已注册 + if skip_registered: + existing_account = db.query(Account).filter( + Account.email == email + ).first() + + if existing_account: + batch_tasks[batch_id]["logs"].append(f"[跳过] {email} 已注册 (账号 ID: {existing_account.id})") + batch_tasks[batch_id]["skipped"] += 1 + batch_tasks[batch_id]["completed"] += 1 + continue + + # 创建注册任务 + task_uuid = str(uuid.uuid4()) + task = crud.create_registration_task( + db, + task_uuid=task_uuid, + proxy=proxy, + email_service_id=service_id + ) + + batch_tasks[batch_id]["logs"].append(f"[注册] 开始注册 {email}...") + + # 运行单个注册任务 + await run_registration_task( + task_uuid, "outlook", proxy, None, service_id + ) + + # 更新统计 + with get_db() as db: + task = crud.get_registration_task(db, task_uuid) + if task: + batch_tasks[batch_id]["completed"] += 1 + if task.status == "completed": + batch_tasks[batch_id]["success"] += 1 + batch_tasks[batch_id]["logs"].append(f"[成功] {email} 注册成功") + elif task.status == "failed": + batch_tasks[batch_id]["failed"] += 1 + batch_tasks[batch_id]["logs"].append(f"[失败] {email} 注册失败: {task.error_message}") + + # 如果不是最后一个任务,等待随机间隔 + if i < len(service_ids) - 1 and not batch_tasks[batch_id]["cancelled"]: + wait_time = random.randint(interval_min, interval_max) + logger.info(f"Outlook 批量任务 {batch_id}: 等待 {wait_time} 秒后继续下一个任务") + await asyncio.sleep(wait_time) + + logger.info(f"Outlook 批量任务 {batch_id} 完成: 成功 {batch_tasks[batch_id]['success']}, 失败 {batch_tasks[batch_id]['failed']}, 跳过 {batch_tasks[batch_id]['skipped']}") + + except Exception as e: + logger.error(f"Outlook 批量任务 {batch_id} 异常: {e}") + batch_tasks[batch_id]["logs"].append(f"[错误] 批量任务异常: {str(e)}") + finally: + batch_tasks[batch_id]["finished"] = True + + +@router.post("/outlook-batch", response_model=OutlookBatchRegistrationResponse) +async def start_outlook_batch_registration( + request: OutlookBatchRegistrationRequest, + background_tasks: BackgroundTasks +): + """ + 启动 Outlook 批量注册任务 + + - service_ids: 选中的 EmailService ID 列表 + - skip_registered: 是否自动跳过已注册邮箱(默认 True) + - proxy: 代理地址 + - interval_min: 最小间隔秒数 + - interval_max: 最大间隔秒数 + """ + from ...database.models import EmailService as EmailServiceModel + from ...database.models import Account + + # 验证参数 + if not request.service_ids: + raise HTTPException(status_code=400, detail="请选择至少一个 Outlook 账户") + + if request.interval_min < 0 or request.interval_max < request.interval_min: + raise HTTPException(status_code=400, detail="间隔时间参数无效") + + # 过滤掉已注册的邮箱 + actual_service_ids = request.service_ids + skipped_count = 0 + + if request.skip_registered: + actual_service_ids = [] + with get_db() as db: + for service_id in request.service_ids: + service = db.query(EmailServiceModel).filter( + EmailServiceModel.id == service_id + ).first() + + if not service: + continue + + config = service.config or {} + email = config.get("email") or service.name + + # 检查是否已注册 + existing_account = db.query(Account).filter( + Account.email == email + ).first() + + if existing_account: + skipped_count += 1 + else: + actual_service_ids.append(service_id) + + if not actual_service_ids: + return OutlookBatchRegistrationResponse( + batch_id="", + total=len(request.service_ids), + skipped=skipped_count, + to_register=0, + service_ids=[] + ) + + # 创建批量任务 + batch_id = str(uuid.uuid4()) + + # 初始化批量任务状态 + batch_tasks[batch_id] = { + "total": len(actual_service_ids), + "completed": 0, + "success": 0, + "failed": 0, + "skipped": 0, + "cancelled": False, + "service_ids": actual_service_ids, + "current_index": 0, + "logs": [], + "finished": False + } + + # 在后台运行批量注册 + background_tasks.add_task( + run_outlook_batch_registration, + batch_id, + actual_service_ids, + request.skip_registered, + request.proxy, + request.interval_min, + request.interval_max + ) + + return OutlookBatchRegistrationResponse( + batch_id=batch_id, + total=len(request.service_ids), + skipped=skipped_count, + to_register=len(actual_service_ids), + service_ids=actual_service_ids + ) + + +@router.get("/outlook-batch/{batch_id}") +async def get_outlook_batch_status(batch_id: str): + """获取 Outlook 批量任务状态""" + if batch_id not in batch_tasks: + raise HTTPException(status_code=404, detail="批量任务不存在") + + batch = batch_tasks[batch_id] + return { + "batch_id": batch_id, + "total": batch["total"], + "completed": batch["completed"], + "success": batch["success"], + "failed": batch["failed"], + "skipped": batch.get("skipped", 0), + "current_index": batch["current_index"], + "cancelled": batch["cancelled"], + "finished": batch.get("finished", False), + "logs": batch.get("logs", []), + "progress": f"{batch['completed']}/{batch['total']}" + } diff --git a/static/js/app.js b/static/js/app.js index 538ea29..6dcd544 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -10,6 +10,8 @@ let logPollingInterval = null; let batchPollingInterval = null; let accountsPollingInterval = null; let isBatchMode = false; +let isOutlookBatchMode = false; +let outlookAccounts = []; let availableServices = { tempmail: { available: true, services: [] }, outlook: { available: false, services: [] }, @@ -21,6 +23,7 @@ const elements = { form: document.getElementById('registration-form'), emailService: document.getElementById('email-service'), regMode: document.getElementById('reg-mode'), + regModeGroup: document.getElementById('reg-mode-group'), batchCountGroup: document.getElementById('batch-count-group'), batchCount: document.getElementById('batch-count'), batchOptions: document.getElementById('batch-options'), @@ -47,7 +50,13 @@ const elements = { batchRemaining: document.getElementById('batch-remaining'), // 已注册账号 recentAccountsTable: document.getElementById('recent-accounts-table'), - refreshAccountsBtn: document.getElementById('refresh-accounts-btn') + refreshAccountsBtn: document.getElementById('refresh-accounts-btn'), + // Outlook 批量注册 + outlookBatchSection: document.getElementById('outlook-batch-section'), + outlookAccountsContainer: document.getElementById('outlook-accounts-container'), + outlookIntervalMin: document.getElementById('outlook-interval-min'), + outlookIntervalMax: document.getElementById('outlook-interval-max'), + outlookSkipRegistered: document.getElementById('outlook-skip-registered') }; // 初始化 @@ -136,6 +145,13 @@ function updateEmailServiceOptions() { }); select.appendChild(optgroup); + + // Outlook 批量注册选项 + const batchOption = document.createElement('option'); + batchOption.value = 'outlook_batch:all'; + batchOption.textContent = `📋 Outlook 批量注册 (${availableServices.outlook.count} 个账户)`; + batchOption.dataset.type = 'outlook_batch'; + optgroup.appendChild(batchOption); } else { const optgroup = document.createElement('optgroup'); optgroup.label = '📧 Outlook (未配置)'; @@ -188,6 +204,22 @@ function handleServiceChange(e) { const [type, id] = value.split(':'); const selectedOption = e.target.options[e.target.selectedIndex]; + // 处理 Outlook 批量注册模式 + if (type === 'outlook_batch') { + isOutlookBatchMode = true; + elements.outlookBatchSection.style.display = 'block'; + elements.regModeGroup.style.display = 'none'; + elements.batchCountGroup.style.display = 'none'; + elements.batchOptions.style.display = 'none'; + loadOutlookAccounts(); + addLog('info', '[系统] 已切换到 Outlook 批量注册模式'); + return; + } else { + isOutlookBatchMode = false; + elements.outlookBatchSection.style.display = 'none'; + elements.regModeGroup.style.display = 'block'; + } + // 显示服务信息 if (type === 'outlook') { const service = availableServices.outlook.services.find(s => s.id == id); @@ -221,6 +253,12 @@ async function handleStartRegistration(e) { return; } + // 处理 Outlook 批量注册模式 + if (isOutlookBatchMode) { + await handleOutlookBatchRegistration(); + return; + } + const [emailServiceType, serviceId] = selectedValue.split(':'); // 禁用开始按钮 @@ -595,6 +633,8 @@ function resetButtons() { elements.cancelBtn.disabled = true; currentTask = null; currentBatch = null; + isBatchMode = false; + // 注意:不重置 isOutlookBatchMode,因为用户可能想继续使用 Outlook 批量模式 } // HTML 转义 @@ -604,3 +644,179 @@ function escapeHtml(text) { div.textContent = text; return div.innerHTML; } + + +// ============== Outlook 批量注册功能 ============== + +// 加载 Outlook 账户列表 +async function loadOutlookAccounts() { + try { + elements.outlookAccountsContainer.innerHTML = '