generated from dellevin/template
功能优化
This commit is contained in:
@@ -27,39 +27,3 @@ gunicorn -w 2 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
|
||||
或使用 systemd 设为开机自启。
|
||||
|
||||
## API
|
||||
|
||||
### POST /api/heartbeat
|
||||
心跳上报,App 启动时和每 5 分钟调用一次。
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "device_hash": "设备匿名标识" }
|
||||
```
|
||||
|
||||
### GET /api/stats
|
||||
获取统计数据。
|
||||
|
||||
响应:
|
||||
```json
|
||||
{ "total_users": 10, "online_users": 3 }
|
||||
```
|
||||
|
||||
- `total_users`: 历史总设备数
|
||||
- `online_users`: 最近 5 分钟内有心跳的设备数
|
||||
|
||||
## 数据存储
|
||||
|
||||
SQLite 数据库 `stats.db`,结构:
|
||||
|
||||
```sql
|
||||
CREATE TABLE devices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_hash TEXT UNIQUE NOT NULL, -- SHA256 哈希后的设备标识
|
||||
first_seen TEXT NOT NULL, -- 首次出现时间
|
||||
last_seen TEXT NOT NULL -- 最后心跳时间
|
||||
);
|
||||
```
|
||||
|
||||
所有数据均为匿名,不包含任何设备原始信息。
|
||||
|
||||
117
server/app.py
117
server/app.py
@@ -1,104 +1,29 @@
|
||||
"""
|
||||
MookNote 用户统计服务
|
||||
Flask + SQLite,匿名统计设备数和在线数
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import hashlib
|
||||
"""MookNote 服务端 - 入口"""
|
||||
import os
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from flask import Flask, request, jsonify
|
||||
from flask import Flask
|
||||
from config import JWT_SECRET
|
||||
from database import init_db
|
||||
from auth import register_auth_routes
|
||||
from admin_api import register_admin_routes
|
||||
from sync_api import register_sync_routes
|
||||
from data_api import register_data_routes
|
||||
from web_ui import register_web_routes
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["SECRET_KEY"] = JWT_SECRET
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stats.db")
|
||||
ONLINE_THRESHOLD_MINUTES = 5 # 超过此时间未心跳视为离线
|
||||
# 初始化数据库
|
||||
init_db()
|
||||
|
||||
|
||||
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 ───────────────────────────────────────────────────────────────────
|
||||
# 注册所有路由模块
|
||||
register_auth_routes(app)
|
||||
register_admin_routes(app)
|
||||
register_sync_routes(app)
|
||||
register_data_routes(app)
|
||||
register_web_routes(app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
from waitress import serve
|
||||
port = int(os.environ.get("PORT", 5000))
|
||||
app.run(host="0.0.0.0", port=port, debug=False)
|
||||
print(f"MookNote 服务端启动于 http://0.0.0.0:{port}")
|
||||
serve(app, host="0.0.0.0", port=port)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
flask==3.1.0
|
||||
pyjwt==2.8.0
|
||||
waitress==3.0.0
|
||||
|
||||
BIN
server/stats.db
BIN
server/stats.db
Binary file not shown.
Reference in New Issue
Block a user