diff --git a/browser_installer.py b/browser_installer.py index 778e715..b940397 100755 --- a/browser_installer.py +++ b/browser_installer.py @@ -6,8 +6,6 @@ """ import os - import time - time.sleep(delay) import sys import shutil import subprocess @@ -74,7 +72,6 @@ class BrowserInstaller: browser = p.chromium.launch(headless=True, timeout=5000) browser.close() self.log("✓ Chromium浏览器已安装且可用") - self._cleanup_zombie_processes() return True except Exception as e: error_msg = str(e) @@ -84,30 +81,11 @@ class BrowserInstaller: if "Executable doesn't exist" in error_msg: self.log("检测到浏览器文件缺失,需要重新安装") - self._cleanup_zombie_processes() return False except Exception as e: - self._cleanup_zombie_processes() self.log(f"✗ 检查浏览器时出错: {str(e)}") return False - - def _cleanup_zombie_processes(self, delay=1): - """清理僵尸子进程""" - try: - import os - import time - time.sleep(delay) - while True: - try: - pid, status = os.waitpid(-1, os.WNOHANG) - if pid == 0: - break - except ChildProcessError: - break - except Exception: - pass - def install_chromium(self): """安装Chromium浏览器""" try: diff --git a/database.py b/database.py index ae21b1d..1b38dbe 100755 --- a/database.py +++ b/database.py @@ -837,7 +837,7 @@ def update_user_email(user_id, email, verified=False): # 先检查email_verified字段是否存在,不存在则添加 try: cursor.execute('SELECT email_verified FROM users LIMIT 1') - except Exception: + except: cursor.execute('ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0') conn.commit() @@ -857,7 +857,7 @@ def update_user_email_notify(user_id, enabled): # 先检查字段是否存在 try: cursor.execute('SELECT email_notify_enabled FROM users LIMIT 1') - except Exception: + except: cursor.execute('ALTER TABLE users ADD COLUMN email_notify_enabled INTEGER DEFAULT 1') conn.commit() @@ -881,7 +881,7 @@ def get_user_email_notify(user_id): if row is None: return True return bool(row[0]) if row[0] is not None else True - except Exception: + except: return True # 字段不存在时默认开启 @@ -1738,7 +1738,7 @@ def get_schedule_by_id(schedule_id): def create_user_schedule(user_id, name='我的定时任务', schedule_time='08:00', weekdays='1,2,3,4,5', browse_type='应读', - enable_screenshot=1, account_ids=None, random_delay=0): + enable_screenshot=1, account_ids=None): """创建用户定时任务""" import json with db_pool.get_db() as conn: @@ -1751,10 +1751,10 @@ def create_user_schedule(user_id, name='我的定时任务', schedule_time='08:0 cursor.execute(''' INSERT INTO user_schedules ( user_id, name, enabled, schedule_time, weekdays, - browse_type, enable_screenshot, account_ids, random_delay, created_at, updated_at - ) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?) + browse_type, enable_screenshot, account_ids, created_at, updated_at + ) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?) ''', (user_id, name, schedule_time, weekdays, browse_type, - enable_screenshot, account_ids_str, random_delay, cst_time, cst_time)) + enable_screenshot, account_ids_str, cst_time, cst_time)) conn.commit() return cursor.lastrowid @@ -1771,7 +1771,7 @@ def update_user_schedule(schedule_id, **kwargs): params = [] allowed_fields = ['name', 'enabled', 'schedule_time', 'weekdays', - 'browse_type', 'enable_screenshot', 'account_ids', 'random_delay'] + 'browse_type', 'enable_screenshot', 'account_ids'] for field in allowed_fields: if field in kwargs: diff --git a/email_service.py b/email_service.py index 1272da4..a2e08c9 100644 --- a/email_service.py +++ b/email_service.py @@ -78,9 +78,6 @@ QUEUE_MAX_SIZE = int(os.environ.get('EMAIL_QUEUE_MAX_SIZE', '100')) # 为安全起见,设置为10MB,超过则分批发送 MAX_ATTACHMENT_SIZE = int(os.environ.get('EMAIL_MAX_ATTACHMENT_SIZE', str(10 * 1024 * 1024))) # 10MB -# SMTP配置获取锁(防止并发获取时竞态条件导致超过每日限额) -_smtp_config_lock = threading.Lock() - # ============ 数据库操作 ============ @@ -503,97 +500,80 @@ def set_primary_smtp_config(config_id: int) -> bool: def _get_available_smtp_config(failover: bool = True) -> Optional[Dict[str, Any]]: """ - 获取可用的SMTP配置(线程安全) + 获取可用的SMTP配置 优先级: 主配置 > 按priority排序的启用配置 - 使用锁保护防止并发获取时超过每日限额 """ today = datetime.now().strftime('%Y-%m-%d') - with _smtp_config_lock: # 使用锁保护整个获取过程 - with db_pool.get_db() as conn: - cursor = conn.cursor() + with db_pool.get_db() as conn: + cursor = conn.cursor() - # 先重置过期的每日计数 - cursor.execute(""" - UPDATE smtp_configs - SET daily_sent = 0, daily_reset_date = ? - WHERE daily_reset_date != ? OR daily_reset_date IS NULL OR daily_reset_date = '' - """, (today, today)) - conn.commit() + # 先重置过期的每日计数 + cursor.execute(""" + UPDATE smtp_configs + SET daily_sent = 0, daily_reset_date = ? + WHERE daily_reset_date != ? OR daily_reset_date IS NULL OR daily_reset_date = '' + """, (today, today)) + conn.commit() - # 获取所有启用的配置,按优先级排序 - cursor.execute(""" - SELECT id, name, host, port, username, password, use_ssl, use_tls, - sender_name, sender_email, daily_limit, daily_sent, is_primary - FROM smtp_configs - WHERE enabled = 1 - ORDER BY is_primary DESC, priority ASC, id ASC - """) + # 获取所有启用的配置,按优先级排序 + cursor.execute(""" + SELECT id, name, host, port, username, password, use_ssl, use_tls, + sender_name, sender_email, daily_limit, daily_sent, is_primary + FROM smtp_configs + WHERE enabled = 1 + ORDER BY is_primary DESC, priority ASC, id ASC + """) - configs = cursor.fetchall() + configs = cursor.fetchall() - for row in configs: - config_id, name, host, port, username, password, use_ssl, use_tls, \ - sender_name, sender_email, daily_limit, daily_sent, is_primary = row + for row in configs: + config_id, name, host, port, username, password, use_ssl, use_tls, \ + sender_name, sender_email, daily_limit, daily_sent, is_primary = row - # 检查每日限额 - if daily_limit > 0 and daily_sent >= daily_limit: - continue # 超过限额,跳过此配置 + # 检查每日限额 + if daily_limit > 0 and daily_sent >= daily_limit: + continue # 超过限额,跳过此配置 - # 预增计数(在返回配置前先占用配额,防止并发超限) - # 如果发送失败,_update_smtp_stats会在失败时回退 - cursor.execute(""" - UPDATE smtp_configs - SET daily_sent = daily_sent + 1 - WHERE id = ? - """, (config_id,)) - conn.commit() + # 解密密码 + decrypted_password = decrypt_password(password) if password else '' - # 解密密码 - decrypted_password = decrypt_password(password) if password else '' + return { + 'id': config_id, + 'name': name, + 'host': host, + 'port': port, + 'username': username, + 'password': decrypted_password, + 'use_ssl': bool(use_ssl), + 'use_tls': bool(use_tls), + 'sender_name': sender_name, + 'sender_email': sender_email, + 'is_primary': bool(is_primary) + } - return { - 'id': config_id, - 'name': name, - 'host': host, - 'port': port, - 'username': username, - 'password': decrypted_password, - 'use_ssl': bool(use_ssl), - 'use_tls': bool(use_tls), - 'sender_name': sender_name, - 'sender_email': sender_email, - 'is_primary': bool(is_primary) - } - - return None + return None def _update_smtp_stats(config_id: int, success: bool, error: str = ''): - """更新SMTP配置的统计信息 - - 注意:daily_sent已在_get_available_smtp_config中预增, - 成功时只更新success_count,失��时需要回退daily_sent - """ + """更新SMTP配置的统计信息""" with db_pool.get_db() as conn: cursor = conn.cursor() if success: - # 成功:只更新成功计数(daily_sent已在获取配置时预增) cursor.execute(""" UPDATE smtp_configs - SET success_count = success_count + 1, + SET daily_sent = daily_sent + 1, + success_count = success_count + 1, last_success_at = CURRENT_TIMESTAMP, last_error = '', updated_at = CURRENT_TIMESTAMP WHERE id = ? """, (config_id,)) else: - # 失败:回退daily_sent并更新失败计数 cursor.execute(""" UPDATE smtp_configs - SET daily_sent = MAX(0, daily_sent - 1), - fail_count = fail_count + 1, + SET fail_count = fail_count + 1, last_error = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? @@ -785,59 +765,46 @@ def send_email( def _get_next_available_smtp_config(exclude_ids: List[int]) -> Optional[Dict[str, Any]]: - """获取下一个可用的SMTP配置(排除已尝试的,线程安全)""" + """获取下一个可用的SMTP配置(排除已尝试的)""" today = datetime.now().strftime('%Y-%m-%d') - with _smtp_config_lock: # 使用锁保护 - with db_pool.get_db() as conn: - cursor = conn.cursor() + with db_pool.get_db() as conn: + cursor = conn.cursor() - placeholders = ','.join(['?' for _ in exclude_ids]) - cursor.execute(f""" - SELECT id, name, host, port, username, password, use_ssl, use_tls, - sender_name, sender_email, daily_limit, daily_sent, is_primary - FROM smtp_configs - WHERE enabled = 1 AND id NOT IN ({placeholders}) - ORDER BY is_primary DESC, priority ASC, id ASC - LIMIT 1 - """, exclude_ids) + placeholders = ','.join(['?' for _ in exclude_ids]) + cursor.execute(f""" + SELECT id, name, host, port, username, password, use_ssl, use_tls, + sender_name, sender_email, daily_limit, daily_sent, is_primary + FROM smtp_configs + WHERE enabled = 1 AND id NOT IN ({placeholders}) + ORDER BY is_primary DESC, priority ASC, id ASC + LIMIT 1 + """, exclude_ids) - row = cursor.fetchone() - if not row: - return None + row = cursor.fetchone() + if not row: + return None - config_id, name, host, port, username, password, use_ssl, use_tls, \ - sender_name, sender_email, daily_limit, daily_sent, is_primary = row + config_id, name, host, port, username, password, use_ssl, use_tls, \ + sender_name, sender_email, daily_limit, daily_sent, is_primary = row - # 检查每日限额 - if daily_limit > 0 and daily_sent >= daily_limit: - # 递归调用在锁外进行,避免死锁 - pass - else: - # 预增计数 - cursor.execute(""" - UPDATE smtp_configs - SET daily_sent = daily_sent + 1 - WHERE id = ? - """, (config_id,)) - conn.commit() + # 检查每日限额 + if daily_limit > 0 and daily_sent >= daily_limit: + return _get_next_available_smtp_config(exclude_ids + [config_id]) - return { - 'id': config_id, - 'name': name, - 'host': host, - 'port': port, - 'username': username, - 'password': decrypt_password(password) if password else '', - 'use_ssl': bool(use_ssl), - 'use_tls': bool(use_tls), - 'sender_name': sender_name, - 'sender_email': sender_email, - 'is_primary': bool(is_primary) - } - - # 递归调用在锁外进行 - return _get_next_available_smtp_config(exclude_ids + [config_id]) + return { + 'id': config_id, + 'name': name, + 'host': host, + 'port': port, + 'username': username, + 'password': decrypt_password(password) if password else '', + 'use_ssl': bool(use_ssl), + 'use_tls': bool(use_tls), + 'sender_name': sender_name, + 'sender_email': sender_email, + 'is_primary': bool(is_primary) + } def test_smtp_config(config_id: int, test_email: str) -> Dict[str, Any]: @@ -2112,8 +2079,22 @@ def send_batch_task_complete_email( ) if result['success']: + # 记录发送日志 + log_email_send( + email_type='batch_task_complete', + to_email=email, + subject=f'定时任务完成 - {schedule_name}', + success=True + ) return {'success': True} else: + log_email_send( + email_type='batch_task_complete', + to_email=email, + subject=f'定时任务完成 - {schedule_name}', + success=False, + error=result.get('error', '') + ) return {'success': False, 'error': result.get('error', '发送失败')} diff --git a/playwright_automation.py b/playwright_automation.py index 4b2606a..38cd347 100755 --- a/playwright_automation.py +++ b/playwright_automation.py @@ -802,6 +802,20 @@ class PlaywrightAutomation: if rows_count == 0: self.log("当前页面没有内容") + # 调试:输出页面信息帮助诊断 + try: + page_html = self.page.content() + if 'ltable' in page_html: + self.log(f"[调试] 表格存在,但没有数据行") + # 检查是否有"暂无记录"提示 + if '暂无' in page_html or '没有' in page_html: + self.log(f"[调试] 页面显示暂无记录") + else: + self.log(f"[调试] 页面中没有找到ltable表格") + # 检查URL + self.log(f"[调试] iframe URL: {self.page.url}") + except Exception as debug_e: + self.log(f"[调试] 获取页面信息失败: {str(debug_e)[:50]}") empty_page_counter += 1 self.log(f"连续空页面数: {empty_page_counter}") @@ -1438,7 +1452,7 @@ class PlaywrightAutomation: try: import os import signal - os.kill(browser_pid, signal.SIGTERM); import time; time.sleep(0.5); os.waitpid(browser_pid, os.WNOHANG) + os.kill(browser_pid, signal.SIGKILL) except (ProcessLookupError, PermissionError, OSError): pass # 进程可能已经退出