109 lines
3.9 KiB
Python
109 lines
3.9 KiB
Python
import json
|
|
import os
|
|
import time
|
|
from threading import Lock
|
|
import atexit
|
|
|
|
# --- 配置 ---
|
|
TIMEOUT_SECONDS = 300 # 5分钟无活动视为离线
|
|
STATS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data_usage")
|
|
STATS_FILE = os.path.join(STATS_DIR, "usage_stats.json")
|
|
SAVE_INTERVAL_SECONDS = int(os.environ.get("USAGE_SAVE_INTERVAL", "60"))
|
|
|
|
# --- 全局变量 ---
|
|
active_users = {} # {session_id: last_active_timestamp}
|
|
users_lock = Lock()
|
|
stats_lock = Lock()
|
|
total_users = 0
|
|
seen_sessions = set()
|
|
_LAST_SAVE_TS = 0.0
|
|
|
|
# --- 内部函数 ---
|
|
def _ensure_stats_dir():
|
|
try:
|
|
os.makedirs(STATS_DIR, exist_ok=True)
|
|
except OSError:
|
|
pass
|
|
|
|
def _load_usage_stats():
|
|
global total_users, _LAST_SAVE_TS
|
|
try:
|
|
if os.path.exists(STATS_FILE):
|
|
with open(STATS_FILE, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
total_users = int(data.get("total_users", 0))
|
|
_LAST_SAVE_TS = float(data.get("last_saved_at", time.time()))
|
|
except (IOError, json.JSONDecodeError):
|
|
total_users = 0
|
|
_LAST_SAVE_TS = time.time()
|
|
|
|
def _save_usage_stats():
|
|
global _LAST_SAVE_TS
|
|
try:
|
|
with stats_lock:
|
|
tmp_path = STATS_FILE + ".tmp"
|
|
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
json.dump({"total_users": total_users, "last_saved_at": time.time()}, f, ensure_ascii=False, indent=2)
|
|
os.replace(tmp_path, STATS_FILE)
|
|
_LAST_SAVE_TS = time.time()
|
|
except (IOError, OSError):
|
|
pass
|
|
|
|
def _maybe_save_usage_stats():
|
|
if time.time() - _LAST_SAVE_TS >= SAVE_INTERVAL_SECONDS:
|
|
_save_usage_stats()
|
|
|
|
# --- 外部接口 ---
|
|
def update_user_activity(session_id: str):
|
|
"""更新用户活跃时间并在首次出现时累计总人数。"""
|
|
current_time = time.time()
|
|
with users_lock:
|
|
active_users[session_id] = current_time
|
|
|
|
with stats_lock:
|
|
global total_users
|
|
if session_id not in seen_sessions:
|
|
seen_sessions.add(session_id)
|
|
total_users += 1
|
|
|
|
def get_active_users_count():
|
|
"""获取当前活跃用户数量"""
|
|
current_time = time.time()
|
|
with users_lock:
|
|
expired_users = [uid for uid, last_time in active_users.items() if current_time - last_time > TIMEOUT_SECONDS]
|
|
for uid in expired_users:
|
|
del active_users[uid]
|
|
return len(active_users)
|
|
|
|
def get_online_status_html():
|
|
"""生成在线人数与总人数的显示HTML,并按需触发持久化。"""
|
|
count = get_active_users_count()
|
|
with stats_lock:
|
|
total = total_users
|
|
|
|
_maybe_save_usage_stats()
|
|
|
|
return f"""
|
|
<div style='display: inline-flex; align-items: center; gap: 10px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
padding: 8px 16px; border-radius: 20px;
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);'>
|
|
<span style='font-size: 1.2em;'>🌐</span>
|
|
<span style='color: white; font-weight: 600; font-size: 1em;'>在线人数:</span>
|
|
<span style='color: #fff; font-weight: 700; font-size: 1.2em;
|
|
background: rgba(255,255,255,0.2); padding: 2px 12px;
|
|
border-radius: 12px; min-width: 30px; text-align: center;'>{count}</span>
|
|
<span style='opacity: 0.4; color: #fff;'> | </span>
|
|
<span style='font-size: 1.2em;'>👥</span>
|
|
<span style='color: white; font-weight: 600; font-size: 1em;'>总人数:</span>
|
|
<span style='color: #fff; font-weight: 700; font-size: 1.2em;
|
|
background: rgba(255,255,255,0.2); padding: 2px 12px;
|
|
border-radius: 12px; min-width: 30px; text-align: center;'>{total}</span>
|
|
</div>
|
|
"""
|
|
|
|
# --- 初始化 ---
|
|
_ensure_stats_dir()
|
|
_load_usage_stats()
|
|
atexit.register(_save_usage_stats) # 确保程序退出时保存数据
|