generated from dellevin/template
105 lines
3.1 KiB
Python
105 lines
3.1 KiB
Python
"""
|
||
MookNote 用户统计服务
|
||
Flask + SQLite,匿名统计设备数和在线数
|
||
"""
|
||
|
||
import sqlite3
|
||
import hashlib
|
||
import os
|
||
from datetime import datetime, timezone, timedelta
|
||
|
||
from flask import Flask, request, jsonify
|
||
|
||
app = Flask(__name__)
|
||
|
||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stats.db")
|
||
ONLINE_THRESHOLD_MINUTES = 5 # 超过此时间未心跳视为离线
|
||
|
||
|
||
def get_db() -> sqlite3.Connection:
|
||
conn = sqlite3.connect(DB_PATH)
|
||
conn.row_factory = sqlite3.Row
|
||
return conn
|
||
|
||
|
||
def init_db():
|
||
with get_db() as conn:
|
||
conn.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS devices (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
device_hash TEXT UNIQUE NOT NULL,
|
||
first_seen TEXT NOT NULL,
|
||
last_seen TEXT NOT NULL
|
||
)
|
||
"""
|
||
)
|
||
conn.execute(
|
||
"CREATE INDEX IF NOT EXISTS idx_last_seen ON devices(last_seen)"
|
||
)
|
||
conn.commit()
|
||
|
||
|
||
# ─── API ────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
@app.route("/api/heartbeat", methods=["POST"])
|
||
def heartbeat():
|
||
"""接收匿名心跳"""
|
||
data = request.get_json(silent=True) or {}
|
||
device_hash = data.get("device_hash", "").strip()
|
||
if not device_hash:
|
||
return jsonify({"error": "device_hash is required"}), 400
|
||
|
||
# 只存哈希,不存原始设备信息
|
||
h = hashlib.sha256(device_hash.encode()).hexdigest()
|
||
now_iso = datetime.now(timezone.utc).isoformat()
|
||
|
||
with get_db() as conn:
|
||
row = conn.execute(
|
||
"SELECT id FROM devices WHERE device_hash = ?", (h,)
|
||
).fetchone()
|
||
|
||
if row:
|
||
conn.execute(
|
||
"UPDATE devices SET last_seen = ? WHERE device_hash = ?",
|
||
(now_iso, h),
|
||
)
|
||
else:
|
||
conn.execute(
|
||
"INSERT INTO devices (device_hash, first_seen, last_seen) VALUES (?, ?, ?)",
|
||
(h, now_iso, now_iso),
|
||
)
|
||
conn.commit()
|
||
|
||
return jsonify({"status": "ok"})
|
||
|
||
|
||
@app.route("/api/stats", methods=["GET"])
|
||
def stats():
|
||
"""获取统计:总用户数和当前在线数"""
|
||
threshold = (
|
||
datetime.now(timezone.utc) - timedelta(minutes=ONLINE_THRESHOLD_MINUTES)
|
||
).isoformat()
|
||
|
||
with get_db() as conn:
|
||
total = conn.execute("SELECT COUNT(*) FROM devices").fetchone()[0]
|
||
online = conn.execute(
|
||
"SELECT COUNT(*) FROM devices WHERE last_seen >= ?", (threshold,)
|
||
).fetchone()[0]
|
||
|
||
return jsonify({"total_users": total, "online_users": online})
|
||
|
||
|
||
@app.route("/", methods=["GET"])
|
||
def index():
|
||
return "MookNote Stats Server is running."
|
||
|
||
|
||
# ─── MAIN ───────────────────────────────────────────────────────────────────
|
||
|
||
if __name__ == "__main__":
|
||
init_db()
|
||
port = int(os.environ.get("PORT", 5000))
|
||
app.run(host="0.0.0.0", port=port, debug=False)
|