refactor: optimize structure, stability and runtime performance
This commit is contained in:
@@ -27,6 +27,12 @@ from services.time_utils import get_beijing_now
|
||||
logger = get_logger("app")
|
||||
config = get_config()
|
||||
|
||||
try:
|
||||
_SCHEDULE_SUBMIT_DELAY_SECONDS = float(os.environ.get("SCHEDULE_SUBMIT_DELAY_SECONDS", "0.2"))
|
||||
except Exception:
|
||||
_SCHEDULE_SUBMIT_DELAY_SECONDS = 0.2
|
||||
_SCHEDULE_SUBMIT_DELAY_SECONDS = max(0.0, _SCHEDULE_SUBMIT_DELAY_SECONDS)
|
||||
|
||||
SCREENSHOTS_DIR = config.SCREENSHOTS_DIR
|
||||
os.makedirs(SCREENSHOTS_DIR, exist_ok=True)
|
||||
|
||||
@@ -55,6 +61,150 @@ def _normalize_hhmm(value: object, *, default: str) -> str:
|
||||
return f"{hour:02d}:{minute:02d}"
|
||||
|
||||
|
||||
def _safe_recompute_schedule_next_run(schedule_id: int) -> None:
|
||||
try:
|
||||
database.recompute_schedule_next_run(schedule_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _load_accounts_for_users(approved_users: list[dict]) -> tuple[dict[int, dict], list[str]]:
|
||||
"""批量加载用户账号快照。"""
|
||||
user_accounts: dict[int, dict] = {}
|
||||
account_ids: list[str] = []
|
||||
for user in approved_users:
|
||||
user_id = user["id"]
|
||||
accounts = safe_get_user_accounts_snapshot(user_id)
|
||||
if not accounts:
|
||||
load_user_accounts(user_id)
|
||||
accounts = safe_get_user_accounts_snapshot(user_id)
|
||||
if accounts:
|
||||
user_accounts[user_id] = accounts
|
||||
account_ids.extend(list(accounts.keys()))
|
||||
return user_accounts, account_ids
|
||||
|
||||
|
||||
def _should_skip_suspended_account(account_status_info, account, username: str) -> bool:
|
||||
"""判断是否应跳过暂停账号,并输出日志。"""
|
||||
if not account_status_info:
|
||||
return False
|
||||
|
||||
status = account_status_info["status"] if "status" in account_status_info.keys() else "active"
|
||||
if status != "suspended":
|
||||
return False
|
||||
|
||||
fail_count = account_status_info["login_fail_count"] if "login_fail_count" in account_status_info.keys() else 0
|
||||
logger.info(
|
||||
f"[定时任务] 跳过暂停账号: {account.username} (用户:{username}) - 连续{fail_count}次密码错误,需修改密码"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _parse_schedule_account_ids(schedule_config: dict, schedule_id: int):
|
||||
import json
|
||||
|
||||
try:
|
||||
account_ids_raw = schedule_config.get("account_ids", "[]") or "[]"
|
||||
account_ids = json.loads(account_ids_raw)
|
||||
except Exception as e:
|
||||
logger.warning(f"[定时任务] 任务#{schedule_id} 解析account_ids失败: {e}")
|
||||
return []
|
||||
if isinstance(account_ids, list):
|
||||
return account_ids
|
||||
return []
|
||||
|
||||
|
||||
def _create_user_schedule_batch(*, batch_id: str, user_id: int, browse_type: str, schedule_name: str, now_ts: float) -> None:
|
||||
safe_create_batch(
|
||||
batch_id,
|
||||
{
|
||||
"user_id": user_id,
|
||||
"browse_type": browse_type,
|
||||
"schedule_name": schedule_name,
|
||||
"screenshots": [],
|
||||
"total_accounts": 0,
|
||||
"completed": 0,
|
||||
"created_at": now_ts,
|
||||
"updated_at": now_ts,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _build_user_schedule_done_callback(
|
||||
*,
|
||||
completion_lock: threading.Lock,
|
||||
remaining: dict,
|
||||
counters: dict,
|
||||
execution_start_time: float,
|
||||
log_id: int,
|
||||
schedule_id: int,
|
||||
total_accounts: int,
|
||||
):
|
||||
def on_browse_done():
|
||||
with completion_lock:
|
||||
remaining["count"] -= 1
|
||||
if remaining["done"] or remaining["count"] > 0:
|
||||
return
|
||||
remaining["done"] = True
|
||||
|
||||
execution_duration = int(time.time() - execution_start_time)
|
||||
started_count = int(counters.get("started", 0) or 0)
|
||||
database.update_schedule_execution_log(
|
||||
log_id,
|
||||
total_accounts=total_accounts,
|
||||
success_accounts=started_count,
|
||||
failed_accounts=total_accounts - started_count,
|
||||
duration_seconds=execution_duration,
|
||||
status="completed",
|
||||
)
|
||||
logger.info(f"[用户定时任务] 任务#{schedule_id}浏览阶段完成,耗时{execution_duration}秒,等待截图完成后发送邮件")
|
||||
|
||||
return on_browse_done
|
||||
|
||||
|
||||
def _submit_user_schedule_accounts(
|
||||
*,
|
||||
user_id: int,
|
||||
account_ids: list,
|
||||
browse_type: str,
|
||||
enable_screenshot,
|
||||
task_source: str,
|
||||
done_callback,
|
||||
completion_lock: threading.Lock,
|
||||
remaining: dict,
|
||||
counters: dict,
|
||||
) -> tuple[int, int]:
|
||||
started_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for account_id in account_ids:
|
||||
account = safe_get_account(user_id, account_id)
|
||||
if (not account) or account.is_running:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
with completion_lock:
|
||||
remaining["count"] += 1
|
||||
ok, msg = submit_account_task(
|
||||
user_id=user_id,
|
||||
account_id=account_id,
|
||||
browse_type=browse_type,
|
||||
enable_screenshot=enable_screenshot,
|
||||
source=task_source,
|
||||
done_callback=done_callback,
|
||||
)
|
||||
if ok:
|
||||
started_count += 1
|
||||
counters["started"] = started_count
|
||||
else:
|
||||
with completion_lock:
|
||||
remaining["count"] -= 1
|
||||
skipped_count += 1
|
||||
logger.warning(f"[用户定时任务] 账号 {account.username} 启动失败: {msg}")
|
||||
|
||||
return started_count, skipped_count
|
||||
|
||||
|
||||
def run_scheduled_task(skip_weekday_check: bool = False) -> None:
|
||||
"""执行所有账号的浏览任务(可被手动调用,过滤重复账号)"""
|
||||
try:
|
||||
@@ -87,17 +237,7 @@ def run_scheduled_task(skip_weekday_check: bool = False) -> None:
|
||||
cfg = database.get_system_config()
|
||||
enable_screenshot_scheduled = cfg.get("enable_screenshot", 0) == 1
|
||||
|
||||
user_accounts = {}
|
||||
account_ids = []
|
||||
for user in approved_users:
|
||||
user_id = user["id"]
|
||||
accounts = safe_get_user_accounts_snapshot(user_id)
|
||||
if not accounts:
|
||||
load_user_accounts(user_id)
|
||||
accounts = safe_get_user_accounts_snapshot(user_id)
|
||||
if accounts:
|
||||
user_accounts[user_id] = accounts
|
||||
account_ids.extend(list(accounts.keys()))
|
||||
user_accounts, account_ids = _load_accounts_for_users(approved_users)
|
||||
|
||||
account_statuses = database.get_account_status_batch(account_ids)
|
||||
|
||||
@@ -113,18 +253,8 @@ def run_scheduled_task(skip_weekday_check: bool = False) -> None:
|
||||
continue
|
||||
|
||||
account_status_info = account_statuses.get(str(account_id))
|
||||
if account_status_info:
|
||||
status = account_status_info["status"] if "status" in account_status_info.keys() else "active"
|
||||
if status == "suspended":
|
||||
fail_count = (
|
||||
account_status_info["login_fail_count"]
|
||||
if "login_fail_count" in account_status_info.keys()
|
||||
else 0
|
||||
)
|
||||
logger.info(
|
||||
f"[定时任务] 跳过暂停账号: {account.username} (用户:{user['username']}) - 连续{fail_count}次密码错误,需修改密码"
|
||||
)
|
||||
continue
|
||||
if _should_skip_suspended_account(account_status_info, account, user["username"]):
|
||||
continue
|
||||
|
||||
if account.username in executed_usernames:
|
||||
skipped_duplicates += 1
|
||||
@@ -149,7 +279,8 @@ def run_scheduled_task(skip_weekday_check: bool = False) -> None:
|
||||
else:
|
||||
logger.warning(f"[定时任务] 启动失败({account.username}): {msg}")
|
||||
|
||||
time.sleep(2)
|
||||
if _SCHEDULE_SUBMIT_DELAY_SECONDS > 0:
|
||||
time.sleep(_SCHEDULE_SUBMIT_DELAY_SECONDS)
|
||||
|
||||
logger.info(
|
||||
f"[定时任务] 执行完成 - 总账号数:{total_accounts}, 已执行:{executed_accounts}, 跳过重复:{skipped_duplicates}"
|
||||
@@ -198,15 +329,16 @@ def scheduled_task_worker() -> None:
|
||||
deleted_screenshots = 0
|
||||
if os.path.exists(SCREENSHOTS_DIR):
|
||||
cutoff_time = time.time() - (7 * 24 * 60 * 60)
|
||||
for filename in os.listdir(SCREENSHOTS_DIR):
|
||||
if filename.lower().endswith((".png", ".jpg", ".jpeg")):
|
||||
filepath = os.path.join(SCREENSHOTS_DIR, filename)
|
||||
with os.scandir(SCREENSHOTS_DIR) as entries:
|
||||
for entry in entries:
|
||||
if (not entry.is_file()) or (not entry.name.lower().endswith((".png", ".jpg", ".jpeg"))):
|
||||
continue
|
||||
try:
|
||||
if os.path.getmtime(filepath) < cutoff_time:
|
||||
os.remove(filepath)
|
||||
if entry.stat().st_mtime < cutoff_time:
|
||||
os.remove(entry.path)
|
||||
deleted_screenshots += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"[定时清理] 删除截图失败 {filename}: {str(e)}")
|
||||
logger.warning(f"[定时清理] 删除截图失败 {entry.name}: {str(e)}")
|
||||
|
||||
logger.info(f"[定时清理] 已删除 {deleted_screenshots} 个截图文件")
|
||||
logger.info("[定时清理] 清理完成!")
|
||||
@@ -214,10 +346,97 @@ def scheduled_task_worker() -> None:
|
||||
except Exception as e:
|
||||
logger.exception(f"[定时清理] 清理任务出错: {str(e)}")
|
||||
|
||||
def _parse_due_schedule_weekdays(schedule_config: dict, schedule_id: int):
|
||||
weekdays_str = schedule_config.get("weekdays", "1,2,3,4,5")
|
||||
try:
|
||||
return [int(d) for d in weekdays_str.split(",") if d.strip()]
|
||||
except Exception as e:
|
||||
logger.warning(f"[定时任务] 任务#{schedule_id} 解析weekdays失败: {e}")
|
||||
_safe_recompute_schedule_next_run(schedule_id)
|
||||
return None
|
||||
|
||||
def _execute_due_user_schedule(schedule_config: dict) -> None:
|
||||
schedule_name = schedule_config.get("name", "未命名任务")
|
||||
schedule_id = schedule_config["id"]
|
||||
user_id = schedule_config["user_id"]
|
||||
browse_type = normalize_browse_type(schedule_config.get("browse_type", BROWSE_TYPE_SHOULD_READ))
|
||||
enable_screenshot = schedule_config.get("enable_screenshot", 1)
|
||||
|
||||
account_ids = _parse_schedule_account_ids(schedule_config, schedule_id)
|
||||
if not account_ids:
|
||||
_safe_recompute_schedule_next_run(schedule_id)
|
||||
return
|
||||
|
||||
if not safe_get_user_accounts_snapshot(user_id):
|
||||
load_user_accounts(user_id)
|
||||
|
||||
import uuid
|
||||
|
||||
execution_start_time = time.time()
|
||||
log_id = database.create_schedule_execution_log(
|
||||
schedule_id=schedule_id,
|
||||
user_id=user_id,
|
||||
schedule_name=schedule_name,
|
||||
)
|
||||
|
||||
batch_id = f"batch_{uuid.uuid4().hex[:12]}"
|
||||
now_ts = time.time()
|
||||
_create_user_schedule_batch(
|
||||
batch_id=batch_id,
|
||||
user_id=user_id,
|
||||
browse_type=browse_type,
|
||||
schedule_name=schedule_name,
|
||||
now_ts=now_ts,
|
||||
)
|
||||
|
||||
completion_lock = threading.Lock()
|
||||
remaining = {"count": 0, "done": False}
|
||||
counters = {"started": 0}
|
||||
|
||||
on_browse_done = _build_user_schedule_done_callback(
|
||||
completion_lock=completion_lock,
|
||||
remaining=remaining,
|
||||
counters=counters,
|
||||
execution_start_time=execution_start_time,
|
||||
log_id=log_id,
|
||||
schedule_id=schedule_id,
|
||||
total_accounts=len(account_ids),
|
||||
)
|
||||
|
||||
task_source = f"user_scheduled:{batch_id}"
|
||||
started_count, skipped_count = _submit_user_schedule_accounts(
|
||||
user_id=user_id,
|
||||
account_ids=account_ids,
|
||||
browse_type=browse_type,
|
||||
enable_screenshot=enable_screenshot,
|
||||
task_source=task_source,
|
||||
done_callback=on_browse_done,
|
||||
completion_lock=completion_lock,
|
||||
remaining=remaining,
|
||||
counters=counters,
|
||||
)
|
||||
|
||||
batch_info = safe_finalize_batch_after_dispatch(batch_id, started_count, now_ts=time.time())
|
||||
if batch_info:
|
||||
_send_batch_task_email_if_configured(batch_info)
|
||||
|
||||
database.update_schedule_last_run(schedule_id)
|
||||
|
||||
logger.info(f"[用户定时任务] 已启动 {started_count} 个账号,跳过 {skipped_count} 个账号,批次ID: {batch_id}")
|
||||
if started_count <= 0:
|
||||
database.update_schedule_execution_log(
|
||||
log_id,
|
||||
total_accounts=len(account_ids),
|
||||
success_accounts=0,
|
||||
failed_accounts=len(account_ids),
|
||||
duration_seconds=0,
|
||||
status="completed",
|
||||
)
|
||||
if started_count == 0 and len(account_ids) > 0:
|
||||
logger.warning("[用户定时任务] ⚠️ 警告:所有账号都被跳过了!请检查user_accounts状态")
|
||||
|
||||
def check_user_schedules():
|
||||
"""检查并执行用户定时任务(O-08:next_run_at 索引驱动)。"""
|
||||
import json
|
||||
|
||||
try:
|
||||
now = get_beijing_now()
|
||||
now_str = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -226,145 +445,22 @@ def scheduled_task_worker() -> None:
|
||||
due_schedules = database.get_due_user_schedules(now_str, limit=50) or []
|
||||
|
||||
for schedule_config in due_schedules:
|
||||
schedule_name = schedule_config.get("name", "未命名任务")
|
||||
schedule_id = schedule_config["id"]
|
||||
schedule_name = schedule_config.get("name", "未命名任务")
|
||||
|
||||
weekdays_str = schedule_config.get("weekdays", "1,2,3,4,5")
|
||||
try:
|
||||
allowed_weekdays = [int(d) for d in weekdays_str.split(",") if d.strip()]
|
||||
except Exception as e:
|
||||
logger.warning(f"[定时任务] 任务#{schedule_id} 解析weekdays失败: {e}")
|
||||
try:
|
||||
database.recompute_schedule_next_run(schedule_id)
|
||||
except Exception:
|
||||
pass
|
||||
allowed_weekdays = _parse_due_schedule_weekdays(schedule_config, schedule_id)
|
||||
if allowed_weekdays is None:
|
||||
continue
|
||||
|
||||
if current_weekday not in allowed_weekdays:
|
||||
try:
|
||||
database.recompute_schedule_next_run(schedule_id)
|
||||
except Exception:
|
||||
pass
|
||||
_safe_recompute_schedule_next_run(schedule_id)
|
||||
continue
|
||||
|
||||
logger.info(f"[用户定时任务] 任务#{schedule_id} '{schedule_name}' 到期,开始执行 (next_run_at={schedule_config.get('next_run_at')})")
|
||||
|
||||
user_id = schedule_config["user_id"]
|
||||
schedule_id = schedule_config["id"]
|
||||
browse_type = normalize_browse_type(schedule_config.get("browse_type", BROWSE_TYPE_SHOULD_READ))
|
||||
enable_screenshot = schedule_config.get("enable_screenshot", 1)
|
||||
|
||||
try:
|
||||
account_ids_raw = schedule_config.get("account_ids", "[]") or "[]"
|
||||
account_ids = json.loads(account_ids_raw)
|
||||
except Exception as e:
|
||||
logger.warning(f"[定时任务] 任务#{schedule_id} 解析account_ids失败: {e}")
|
||||
account_ids = []
|
||||
|
||||
if not account_ids:
|
||||
try:
|
||||
database.recompute_schedule_next_run(schedule_id)
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
|
||||
if not safe_get_user_accounts_snapshot(user_id):
|
||||
load_user_accounts(user_id)
|
||||
|
||||
import time as time_mod
|
||||
import uuid
|
||||
|
||||
execution_start_time = time_mod.time()
|
||||
log_id = database.create_schedule_execution_log(
|
||||
schedule_id=schedule_id, user_id=user_id, schedule_name=schedule_config.get("name", "未命名任务")
|
||||
logger.info(
|
||||
f"[用户定时任务] 任务#{schedule_id} '{schedule_name}' 到期,开始执行 "
|
||||
f"(next_run_at={schedule_config.get('next_run_at')})"
|
||||
)
|
||||
|
||||
batch_id = f"batch_{uuid.uuid4().hex[:12]}"
|
||||
now_ts = time_mod.time()
|
||||
safe_create_batch(
|
||||
batch_id,
|
||||
{
|
||||
"user_id": user_id,
|
||||
"browse_type": browse_type,
|
||||
"schedule_name": schedule_config.get("name", "未命名任务"),
|
||||
"screenshots": [],
|
||||
"total_accounts": 0,
|
||||
"completed": 0,
|
||||
"created_at": now_ts,
|
||||
"updated_at": now_ts,
|
||||
},
|
||||
)
|
||||
|
||||
started_count = 0
|
||||
skipped_count = 0
|
||||
completion_lock = threading.Lock()
|
||||
remaining = {"count": 0, "done": False}
|
||||
|
||||
def on_browse_done():
|
||||
with completion_lock:
|
||||
remaining["count"] -= 1
|
||||
if remaining["done"] or remaining["count"] > 0:
|
||||
return
|
||||
remaining["done"] = True
|
||||
execution_duration = int(time_mod.time() - execution_start_time)
|
||||
database.update_schedule_execution_log(
|
||||
log_id,
|
||||
total_accounts=len(account_ids),
|
||||
success_accounts=started_count,
|
||||
failed_accounts=len(account_ids) - started_count,
|
||||
duration_seconds=execution_duration,
|
||||
status="completed",
|
||||
)
|
||||
logger.info(
|
||||
f"[用户定时任务] 任务#{schedule_id}浏览阶段完成,耗时{execution_duration}秒,等待截图完成后发送邮件"
|
||||
)
|
||||
|
||||
for account_id in account_ids:
|
||||
account = safe_get_account(user_id, account_id)
|
||||
if not account:
|
||||
skipped_count += 1
|
||||
continue
|
||||
if account.is_running:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
task_source = f"user_scheduled:{batch_id}"
|
||||
with completion_lock:
|
||||
remaining["count"] += 1
|
||||
ok, msg = submit_account_task(
|
||||
user_id=user_id,
|
||||
account_id=account_id,
|
||||
browse_type=browse_type,
|
||||
enable_screenshot=enable_screenshot,
|
||||
source=task_source,
|
||||
done_callback=on_browse_done,
|
||||
)
|
||||
if ok:
|
||||
started_count += 1
|
||||
else:
|
||||
with completion_lock:
|
||||
remaining["count"] -= 1
|
||||
skipped_count += 1
|
||||
logger.warning(f"[用户定时任务] 账号 {account.username} 启动失败: {msg}")
|
||||
|
||||
batch_info = safe_finalize_batch_after_dispatch(batch_id, started_count, now_ts=time_mod.time())
|
||||
if batch_info:
|
||||
_send_batch_task_email_if_configured(batch_info)
|
||||
|
||||
database.update_schedule_last_run(schedule_id)
|
||||
|
||||
logger.info(f"[用户定时任务] 已启动 {started_count} 个账号,跳过 {skipped_count} 个账号,批次ID: {batch_id}")
|
||||
if started_count <= 0:
|
||||
database.update_schedule_execution_log(
|
||||
log_id,
|
||||
total_accounts=len(account_ids),
|
||||
success_accounts=0,
|
||||
failed_accounts=len(account_ids),
|
||||
duration_seconds=0,
|
||||
status="completed",
|
||||
)
|
||||
if started_count == 0 and len(account_ids) > 0:
|
||||
logger.warning("[用户定时任务] ⚠️ 警告:所有账号都被跳过了!请检查user_accounts状态")
|
||||
_execute_due_user_schedule(schedule_config)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"[用户定时任务] 检查出错: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user