diff --git a/app/db.py b/app/db.py index 2b7a24f..5f23f9b 100644 --- a/app/db.py +++ b/app/db.py @@ -141,7 +141,8 @@ def list_machines(conn: sqlite3.Connection) -> list[dict]: return [row_to_dict(r) for r in cur.fetchall()] -ONLINE_SECONDS = 180 # 与下载中心一致:3 分钟内心跳视为在线 +# 客户端约 2 秒心跳一次;超过该秒数无心跳视为离线(秒级大屏) +ONLINE_SECONDS = 8 def list_machines_for_user(conn: sqlite3.Connection, user: dict) -> list[dict]: diff --git a/app/routes.py b/app/routes.py index 37047a9..34cfab1 100644 --- a/app/routes.py +++ b/app/routes.py @@ -3,11 +3,13 @@ from __future__ import annotations import json import secrets +import time from datetime import datetime from functools import wraps from flask import ( Flask, + Response, flash, g, jsonify, @@ -16,6 +18,7 @@ from flask import ( request, send_from_directory, session, + stream_with_context, url_for, ) @@ -222,42 +225,69 @@ def register_routes(app: Flask) -> None: data = _dashboard_payload(current_user()) return render_template("dashboard.html", **data) + def _dashboard_live_dict(u: dict) -> dict: + data = _dashboard_payload(u) + return { + "ok": True, + "ts": database.now(), + "online_count": data["online_count"], + "machine_count": data["machine_count"], + "online_machines": [ + { + "id": m["id"], + "name": m.get("name") or f"#{m['id']}", + "last_seen_at": m.get("last_seen_at"), + "last_seen_fmt": _fmt_ts(m.get("last_seen_at")), + "online": True, + } + for m in data["online_machines"] + ], + "trades": [ + { + "id": t["id"], + "time_fmt": t.get("time_fmt"), + "symbol": t.get("symbol"), + "side": t.get("side"), + "volume": t.get("volume"), + "profit": t.get("profit"), + "account_name": t.get("account_name"), + "machine_name": t.get("machine_name"), + "ticket": t.get("ticket"), + } + for t in data["trades"] + ], + } + @app.get("/api/dashboard/live") @login_required def dashboard_live(): - """总览轮询:在线机器 + 实时成交。""" - data = _dashboard_payload(current_user()) - return jsonify( - { - "ok": True, - "online_count": data["online_count"], - "machine_count": data["machine_count"], - "online_machines": [ - { - "id": m["id"], - "name": m.get("name") or f"#{m['id']}", - "last_seen_at": m.get("last_seen_at"), - "last_seen_fmt": _fmt_ts(m.get("last_seen_at")), - "online": True, - } - for m in data["online_machines"] - ], - "trades": [ - { - "id": t["id"], - "time_fmt": t.get("time_fmt"), - "symbol": t.get("symbol"), - "side": t.get("side"), - "volume": t.get("volume"), - "profit": t.get("profit"), - "account_name": t.get("account_name"), - "machine_name": t.get("machine_name"), - "ticket": t.get("ticket"), - } - for t in data["trades"] - ], - } - ) + """总览快照(兼容轮询)。""" + return jsonify(_dashboard_live_dict(current_user())) + + @app.get("/api/dashboard/stream") + @login_required + def dashboard_stream(): + """秒级 SSE 推送:在线机器 + 实时成交。""" + u = current_user() + + @stream_with_context + def event_stream(): + # 立即推一帧,后续约每秒一帧 + while True: + try: + payload = json.dumps(_dashboard_live_dict(u), ensure_ascii=False) + yield f"data: {payload}\n\n" + except GeneratorExit: + return + except Exception: + yield 'event: error\ndata: {"ok":false}\n\n' + time.sleep(1) + + resp = Response(event_stream(), mimetype="text/event-stream") + resp.headers["Cache-Control"] = "no-cache" + resp.headers["X-Accel-Buffering"] = "no" + resp.headers["Connection"] = "keep-alive" + return resp # ---------- 账户(仅管理员创建)---------- @app.get("/accounts") @@ -404,7 +434,7 @@ def register_routes(app: Flask) -> None: ).fetchall() activations = [] now_ts = database.now() - online_sec = 180 # 3 分钟内有心跳视为在线 + online_sec = database.ONLINE_SECONDS for r in acts: item = database.row_to_dict(r) last = item.get("machine_last_seen") diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index c510d45..6babdb9 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block title %}总览 · {{ app_name }}{% endblock %} {% block content %} -

总览

+

总览 · 秒级推送

账户
{{ accounts|length }}
@@ -13,7 +13,7 @@
-

在线机器 · 约 5 秒刷新

+

在线机器

@@ -24,7 +24,7 @@ {% else %} - + {% endfor %}
机器状态最近心跳
{{ m.last_seen_fmt }}
当前无在线机器(客户端运行中会每分钟心跳)
当前无在线机器(客户端约每 2 秒心跳)
@@ -41,7 +41,7 @@
-

机器实时交易记录 · 自动刷新

+

机器实时交易记录

@@ -74,8 +74,10 @@ {% block scripts %} {% endblock %} diff --git a/deploy/ecosystem.config.cjs b/deploy/ecosystem.config.cjs index 34ae51d..2ce305d 100644 --- a/deploy/ecosystem.config.cjs +++ b/deploy/ecosystem.config.cjs @@ -11,7 +11,8 @@ module.exports = { name: "gold_monitor", cwd: root, script: path.join(root, ".venv/bin/gunicorn"), - args: "wsgi:app --bind 0.0.0.0:5800 --workers 2 --threads 4 --timeout 120", + // threads 便于 SSE 长连接;timeout 拉长避免秒级推送被 worker 杀掉 + args: "wsgi:app --bind 0.0.0.0:5800 --workers 2 --threads 8 --timeout 300 --keep-alive 5", interpreter: "none", autorestart: true, max_restarts: 20, diff --git a/desktop/README.md b/desktop/README.md index e4fcee1..1caf0f0 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -5,7 +5,7 @@ - 连接本机已登录 MT5,市价开平、ATR 止损止盈、保本、满仓灰显等(由原 `MT5黄金方案/Desktop` 迁入) - **通用包 + 首次激活**:仅第一次填写中控地址与激活码;**不允许离线运行** - **打开复盘**:用已保存的中控地址打开 `/login` -- **心跳**:运行中每分钟 ping 中控,网页下载中心显示在线/离线 +- **心跳**:约每 2 秒 ping 中控,配合总览 SSE 秒级在线状态 - **自动上报**:开仓/平仓成功后上报成交与四周期 K 线 ## 本地配置 diff --git a/desktop/gold_order_app.py b/desktop/gold_order_app.py index 2390587..5c03a08 100644 --- a/desktop/gold_order_app.py +++ b/desktop/gold_order_app.py @@ -1447,23 +1447,22 @@ class OrderApp(tk.Tk): self._run_async(work) def _hub_heartbeat(self) -> None: - """定期 ping 中控,供网页显示在线;断线则提示(不允许离线运行)。""" + """约每 2 秒 ping 中控,供总览秒级在线状态。""" + interval_ms = 2000 if hub_client is None or not is_activated(): - self.after(60000, self._hub_heartbeat) + self.after(interval_ms, self._hub_heartbeat) return def work(): - ok = False try: - ok = bool(hub_client.ping()) + return bool(hub_client.ping()) except Exception: - ok = False - return ok + return False def done(ok: bool): if not ok: self.set_status("中控离线:请检查服务器,客户端需保持在线", ok=False) - self.after(60000, self._hub_heartbeat) + self.after(interval_ms, self._hub_heartbeat) def run(): ok = work() diff --git a/docs/technical.md b/docs/technical.md index 04d1fba..bb9d644 100644 --- a/docs/technical.md +++ b/docs/technical.md @@ -3,11 +3,13 @@ ## 1. 架构 ``` -Browser (复盘/管理) ──HTTP:5800──► Flask(gunicorn) + SQLite +Browser (复盘大屏) ──SSE/HTTP:5800──► Flask(gunicorn threads) + SQLite ▲ -Windows exe (通用包+激活) ──API───────────┘ +Windows exe (心跳约2s + 成交上报) ───────┘ │ └── MetaTrader5 (本机) + +总览秒级:`GET /api/dashboard/stream`(SSE,约 1 秒一帧);在线判定约 8 秒无心跳为离线。 ``` - **不直连数据库**:多机只走 HTTP API。 diff --git a/releases/黄金市价辅助.exe b/releases/黄金市价辅助.exe index 0e0615f..1480fa8 100644 Binary files a/releases/黄金市价辅助.exe and b/releases/黄金市价辅助.exe differ