加入用户统计信息

This commit is contained in:
DelLevin-Home
2026-05-23 00:01:42 +08:00
parent af2ac16521
commit 39df4f9cbe
12 changed files with 497 additions and 15 deletions

65
server/README.md Normal file
View File

@@ -0,0 +1,65 @@
# MookNote 用户统计服务
## 部署
### 1. 安装依赖
```bash
cd server
pip install -r requirements.txt
```
### 2. 启动服务
```bash
python app.py
```
默认监听 `0.0.0.0:5000`,可通过环境变量 `PORT` 修改端口。
### 3. 生产环境部署
推荐使用 gunicorn
```bash
pip install gunicorn
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 -- 最后心跳时间
);
```
所有数据均为匿名,不包含任何设备原始信息。

104
server/app.py Normal file
View File

@@ -0,0 +1,104 @@
"""
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)

1
server/requirements.txt Normal file
View File

@@ -0,0 +1 @@
flask==3.1.0

BIN
server/stats.db Normal file

Binary file not shown.