fix: CTP登录冷却持久化到数据库,取消页面自动连并刷新JS缓存

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 17:01:36 +08:00
parent 9c8b92d2bd
commit 259d9e812d
4 changed files with 80 additions and 17 deletions
+12 -3
View File
@@ -205,7 +205,9 @@ def require_nav(key: str):
@app.context_processor
def inject_globals():
return {"nav_items": get_nav_items(get_setting)}
trade_js = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static", "js", "trade.js")
asset_v = str(int(os.path.getmtime(trade_js))) if os.path.isfile(trade_js) else "0"
return {"nav_items": get_nav_items(get_setting), "asset_v": asset_v}
def _trading_mode() -> str:
@@ -1714,9 +1716,16 @@ def settings():
save_ctp_settings_from_form(request.form, set_setting)
try:
from vnpy_bridge import get_bridge
from vnpy_bridge import get_bridge, _persist_last_error
get_bridge().mark_disconnected()
b = get_bridge()
b.mark_disconnected()
if (request.form.get("simnow_password") or "").strip() or (
request.form.get("ctp_live_password") or ""
).strip():
b._clear_login_cooldown()
b._last_error = ""
_persist_last_error("")
except Exception:
pass
flash("CTP 配置已保存,请在持仓监控页重连 CTP")
+2 -8
View File
@@ -394,7 +394,6 @@
return Promise.resolve({});
}
ctpConnectInflight = true;
updateCtpBadge(false, true);
return fetch('/api/ctp/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -415,6 +414,7 @@
return d;
}
if (d.connecting || st.connecting) {
updateCtpBadge(false, true);
return waitForCtpConnected(70000).then(function (ok) {
if (!ok && d.error) showCtpError(d.error);
else if (!ok && st.last_error) showCtpError(st.last_error);
@@ -1086,13 +1086,7 @@
var st = d.status || {};
syncCtpBadgeFromStatus(st);
if (st.last_error) showCtpError(st.last_error);
if (st.connected) {
pollPositions();
return;
}
if ((st.login_cooldown_sec || 0) > 0) return;
if (isCtpLoginBanError(st.last_error)) return;
if (!st.connecting) requestCtpConnect(false);
if (st.connected) pollPositions();
})
.catch(function () {});
}
+1 -1
View File
@@ -161,5 +161,5 @@ window.TRADE_SIZING_MODE = {{ sizing_mode|tojson }};
window.TRADE_FIXED_LOTS = {{ fixed_lots|tojson }};
window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }};
</script>
<script src="{{ url_for('static', filename='js/trade.js') }}"></script>
<script src="{{ url_for('static', filename='js/trade.js') }}?v={{ asset_v }}"></script>
{% endblock %}
+65 -5
View File
@@ -24,6 +24,48 @@ CONNECT_WAIT_SEC = 60
CONNECT_POLL_INTERVAL_SEC = 0.5
LOGIN_BAN_COOLDOWN_SEC = 45 * 60
LOGIN_FAIL_COOLDOWN_SEC = 5 * 60
CTP_COOLDOWN_UNTIL_KEY = "ctp_login_cooldown_until"
CTP_LAST_ERROR_KEY = "ctp_last_error"
def _persist_login_cooldown(seconds: float) -> None:
from fee_specs import get_setting, set_setting
new_until = time.time() + max(0.0, seconds)
try:
old = float(get_setting(CTP_COOLDOWN_UNTIL_KEY, "0") or 0)
except (TypeError, ValueError):
old = 0.0
if new_until > old:
set_setting(CTP_COOLDOWN_UNTIL_KEY, str(new_until))
def _persisted_login_cooldown_remaining() -> int:
from fee_specs import get_setting
try:
until = float(get_setting(CTP_COOLDOWN_UNTIL_KEY, "0") or 0)
return max(0, int(until - time.time()))
except (TypeError, ValueError):
return 0
def _clear_persisted_login_cooldown() -> None:
from fee_specs import set_setting
set_setting(CTP_COOLDOWN_UNTIL_KEY, "0")
def _persist_last_error(msg: str) -> None:
from fee_specs import set_setting
set_setting(CTP_LAST_ERROR_KEY, (msg or "").strip())
def _load_persisted_last_error() -> str:
from fee_specs import get_setting
return (get_setting(CTP_LAST_ERROR_KEY, "") or "").strip()
_position_refresh_callback: Optional[Callable[[], None]] = None
@@ -138,6 +180,7 @@ class CtpBridge:
self._connect_lock = threading.Lock()
self._connect_in_progress = False
self._login_cooldown_until: float = 0.0
self._restore_persisted_state()
self._commission_waiters: dict[int, threading.Event] = {}
self._commission_lists: dict[int, list] = {}
self._commission_hooked = False
@@ -180,10 +223,18 @@ class CtpBridge:
def connect_in_progress(self) -> bool:
return self._connect_in_progress
def _restore_persisted_state(self) -> None:
err = _load_persisted_last_error()
if err:
self._last_error = err
db_remain = _persisted_login_cooldown_remaining()
if db_remain > 0:
self._login_cooldown_until = time.monotonic() + db_remain
def login_cooldown_remaining(self) -> int:
"""距允许再次登录的剩余秒数。"""
remain = int(self._login_cooldown_until - time.monotonic())
return max(0, remain)
"""距允许再次登录的剩余秒数(内存 + 数据库,重启后仍有效)"""
mem = max(0, int(self._login_cooldown_until - time.monotonic()))
return max(mem, _persisted_login_cooldown_remaining())
def _is_login_cooldown_active(self) -> bool:
return self.login_cooldown_remaining() > 0
@@ -192,6 +243,11 @@ class CtpBridge:
until = time.monotonic() + max(0.0, seconds)
if until > self._login_cooldown_until:
self._login_cooldown_until = until
_persist_login_cooldown(seconds)
def _clear_login_cooldown(self) -> None:
self._login_cooldown_until = 0.0
_clear_persisted_login_cooldown()
def _apply_login_failure_cooldown(self, ctp_logs: list[str]) -> None:
text = "\n".join(ctp_logs)
@@ -253,6 +309,7 @@ class CtpBridge:
missing = [k for k in ("用户名", "密码", "交易服务器") if not st.get(k)]
cooldown = self.login_cooldown_remaining()
connecting = bool(self._connect_in_progress and cooldown <= 0)
last_error = self._last_error or _load_persisted_last_error()
return {
"vnpy_installed": self.available(),
"connected": self._connected_mode == mode,
@@ -260,8 +317,8 @@ class CtpBridge:
"connected_mode": self._connected_mode,
"mode_label": _mode_label(mode),
"missing_config": missing,
"last_error": self._last_error,
"login_cooldown_sec": self.login_cooldown_remaining(),
"last_error": last_error,
"login_cooldown_sec": cooldown,
"broker_id": st.get("经纪商代码", ""),
"td_address": st.get("交易服务器", ""),
}
@@ -336,6 +393,8 @@ class CtpBridge:
if self._wait_connected(mode, ctp_logs):
self._connected_mode = mode
self._last_error = ""
_persist_last_error("")
self._clear_login_cooldown()
logger.info("CTP 已连接 [%s] td_login=%s accounts=%s",
mode, self._td_logged_in(),
len(self._engine.get_all_accounts() or []))
@@ -354,6 +413,7 @@ class CtpBridge:
self._apply_login_failure_cooldown(ctp_logs)
hint = _format_ctp_failure(ctp_logs, td_address=setting.get("交易服务器", ""))
self._last_error = hint
_persist_last_error(hint)
logger.warning("CTP 连接失败 [%s]: %s | logs=%s", mode, hint, ctp_logs[-5:])
raise RuntimeError(hint)
finally: