245b85ad27
Co-authored-by: Cursor <cursoragent@cursor.com>
209 lines
6.6 KiB
Python
209 lines
6.6 KiB
Python
"""三所统一:账户流水路由 + 后台同步安装."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any, Callable
|
|
|
|
from flask import Flask, Response, jsonify, request, session, stream_with_context
|
|
from jinja2 import ChoiceLoader, FileSystemLoader
|
|
|
|
from lib.account_ledger.account_ledger_db import ensure_account_ledger_tables, query_entries
|
|
from lib.account_ledger.account_ledger_normalize import (
|
|
ACCOUNT_FUNDING,
|
|
ACCOUNT_TRADING,
|
|
PAGE_SIZE,
|
|
VALID_ACCOUNTS,
|
|
)
|
|
from lib.account_ledger.account_ledger_sync import account_ledger_store
|
|
from lib.common.history_window_lib import resolve_list_window
|
|
|
|
|
|
def attach_account_ledger_templates(app: Flask, repo_root: str) -> None:
|
|
tpl_dir = os.path.join(repo_root, "lib", "account_ledger", "templates")
|
|
if not os.path.isdir(tpl_dir):
|
|
return
|
|
existing = app.jinja_loader
|
|
loaders = [FileSystemLoader(tpl_dir)]
|
|
if existing is not None:
|
|
if isinstance(existing, ChoiceLoader):
|
|
loaders = list(existing.loaders) + loaders
|
|
else:
|
|
loaders.insert(0, existing)
|
|
app.jinja_loader = ChoiceLoader(loaders)
|
|
|
|
|
|
def _build_fetch_fn(exchange_key: str, app_module: Any) -> Callable:
|
|
ex_key = (exchange_key or "").strip().lower()
|
|
exchange = getattr(app_module, "exchange", None)
|
|
ensure_markets = getattr(app_module, "ensure_markets_loaded", None)
|
|
|
|
def _fetch(*, start_ms: int, end_ms: int):
|
|
if exchange is None:
|
|
return [], ["exchange missing"]
|
|
if ex_key == "okx":
|
|
from lib.exchange.okx_ledger_lib import fetch_okx_account_ledger
|
|
|
|
return fetch_okx_account_ledger(
|
|
exchange,
|
|
start_ms=start_ms,
|
|
end_ms=end_ms,
|
|
ensure_markets=ensure_markets,
|
|
)
|
|
if ex_key == "binance":
|
|
from lib.exchange.binance_ledger_lib import fetch_binance_account_ledger
|
|
|
|
return fetch_binance_account_ledger(
|
|
exchange,
|
|
start_ms=start_ms,
|
|
end_ms=end_ms,
|
|
ensure_markets=ensure_markets,
|
|
)
|
|
from lib.exchange.gate_ledger_lib import fetch_gate_account_ledger
|
|
|
|
return fetch_gate_account_ledger(
|
|
exchange,
|
|
start_ms=start_ms,
|
|
end_ms=end_ms,
|
|
ensure_markets=ensure_markets,
|
|
)
|
|
|
|
return _fetch
|
|
|
|
|
|
def _currencies_for_exchange(exchange_key: str) -> list[str]:
|
|
if (exchange_key or "").strip().lower() == "okx":
|
|
return ["USDT", "USDC"]
|
|
return ["USDT"]
|
|
|
|
|
|
def install_account_ledger(
|
|
app: Flask,
|
|
repo_root: str,
|
|
app_module: Any,
|
|
*,
|
|
exchange_key: str = "",
|
|
) -> None:
|
|
ex = (exchange_key or "").strip().lower()
|
|
if not ex:
|
|
mod_name = getattr(app_module, "__name__", "") or ""
|
|
if "okx" in mod_name.lower():
|
|
ex = "okx"
|
|
elif "binance" in mod_name.lower():
|
|
ex = "binance"
|
|
else:
|
|
ex = "gate"
|
|
exchange_key = ex
|
|
|
|
attach_account_ledger_templates(app, repo_root)
|
|
get_db = app_module.get_db
|
|
login_required = app_module.login_required
|
|
|
|
# 初始化表
|
|
try:
|
|
conn = get_db()
|
|
try:
|
|
ensure_account_ledger_tables(conn)
|
|
finally:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
account_ledger_store.configure(
|
|
get_db=get_db,
|
|
fetch_fn=_build_fetch_fn(exchange_key, app_module),
|
|
exchange_key=str(exchange_key),
|
|
)
|
|
account_ledger_store.start()
|
|
app.extensions["account_ledger_exchange"] = str(exchange_key).lower()
|
|
|
|
def _list_window():
|
|
resolve = getattr(app_module, "_list_window_from_request", None)
|
|
if callable(resolve):
|
|
return resolve()
|
|
return resolve_list_window(request.args, session)
|
|
|
|
@app.route("/api/account_ledger")
|
|
@login_required
|
|
def api_account_ledger():
|
|
account = (request.args.get("account") or ACCOUNT_FUNDING).strip().lower()
|
|
if account not in VALID_ACCOUNTS:
|
|
account = ACCOUNT_FUNDING
|
|
try:
|
|
page = int(request.args.get("page") or 1)
|
|
except Exception:
|
|
page = 1
|
|
win = _list_window()
|
|
start_ms = int(win.get("start_ms") or 0)
|
|
end_ms = int(win.get("end_ms") or 0)
|
|
ccys = _currencies_for_exchange(app.extensions.get("account_ledger_exchange") or "")
|
|
conn = get_db()
|
|
try:
|
|
ensure_account_ledger_tables(conn)
|
|
data = query_entries(
|
|
conn,
|
|
account=account,
|
|
start_ms=start_ms,
|
|
end_ms=end_ms,
|
|
page=page,
|
|
page_size=PAGE_SIZE,
|
|
currencies=ccys,
|
|
)
|
|
finally:
|
|
conn.close()
|
|
st = account_ledger_store.status_dict()
|
|
return jsonify(
|
|
{
|
|
"ok": True,
|
|
"account": account,
|
|
"window": {
|
|
"preset": win.get("preset"),
|
|
"label": win.get("label"),
|
|
"start_ms": start_ms,
|
|
"end_ms": end_ms,
|
|
},
|
|
"currencies": ccys,
|
|
**data,
|
|
**st,
|
|
}
|
|
)
|
|
|
|
@app.route("/api/account_ledger/stream")
|
|
@login_required
|
|
def api_account_ledger_stream():
|
|
return Response(
|
|
stream_with_context(account_ledger_store.iter_sse()),
|
|
mimetype="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
)
|
|
|
|
@app.route("/api/account_ledger/refresh", methods=["POST"])
|
|
@login_required
|
|
def api_account_ledger_refresh():
|
|
win = _list_window()
|
|
body = request.get_json(silent=True) or {}
|
|
start_ms = body.get("start_ms", win.get("start_ms"))
|
|
end_ms = body.get("end_ms", win.get("end_ms"))
|
|
try:
|
|
start_i = int(start_ms) if start_ms is not None else None
|
|
end_i = int(end_ms) if end_ms is not None else None
|
|
except Exception:
|
|
start_i, end_i = None, None
|
|
result = account_ledger_store.sync_once(
|
|
reason="manual", start_ms=start_i, end_ms=end_i
|
|
)
|
|
return jsonify(result)
|
|
|
|
@app.route("/account_ledger")
|
|
@login_required
|
|
def account_ledger_page():
|
|
from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled
|
|
|
|
redir = redirect_to_embed_shell_if_enabled("account_ledger")
|
|
if redir is not None:
|
|
return redir
|
|
return app_module.render_main_page("account_ledger")
|