"""离线检测定时调度器 使用 APScheduler 在 Django 进程中启动后台线程,每 30 秒检测一次终端心跳超时。 """ import logging from apscheduler.schedulers.background import BackgroundScheduler from django.utils.timezone import now logger = logging.getLogger(__name__) CHECK_INTERVAL_SECONDS = 30 # 检测间隔:30 秒 _scheduler: BackgroundScheduler | None = None def _check_offline_terminals(): """检测超时心跳的终端(3 分钟内未心跳),标记为离线并推送 WebSocket 通知""" from datetime import timedelta from apps.core.models import AiSmartTerminal HEARTBEAT_TIMEOUT = timedelta(minutes=3) threshold = now() - HEARTBEAT_TIMEOUT offline_terminals = AiSmartTerminal.objects.filter( online_status=1, last_heartbeat_time__isnull=False, last_heartbeat_time__lt=threshold, ) if not offline_terminals.exists(): return offline_list = list(offline_terminals.values("stcd", "stnm")) count = offline_terminals.update(online_status=0) logger.warning("标记 %d 个终端为离线:", count) for t in offline_list: logger.warning(" - %s (%s)", t["stcd"], t["stnm"]) # WebSocket 推送离线通知 try: from asgiref.sync import async_to_sync from channels.layers import get_channel_layer channel_layer = get_channel_layer() if channel_layer is not None: async_to_sync(channel_layer.group_send)( "terminal_status", { "type": "terminal.status", "data": { "event": "offline", "count": count, "terminals": offline_list, }, }, ) except Exception: logger.exception("WebSocket 推送失败") def start_offline_checker(): """启动离线检测后台调度器(仅在主进程中启动一次)""" global _scheduler if _scheduler is not None: return # 已启动,避免重复 _scheduler = BackgroundScheduler(daemon=True) _scheduler.add_job( _check_offline_terminals, "interval", seconds=CHECK_INTERVAL_SECONDS, id="check_terminal_offline", replace_existing=True, ) _scheduler.start() logger.info("离线检测调度器已启动,每 %d 秒检测一次", CHECK_INTERVAL_SECONDS)