Files
gold_monitor/app/routes.py
T

367 lines
14 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import annotations
import json
import secrets
from functools import wraps
from pathlib import Path
from flask import (
Flask,
flash,
jsonify,
redirect,
render_template,
request,
send_from_directory,
session,
url_for,
)
from app.config import Config, RELEASES_DIR
from app import db as database
def login_required(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not session.get("user"):
return redirect(url_for("login", next=request.path))
return view(*args, **kwargs)
return wrapped
def register_routes(app: Flask) -> None:
@app.context_processor
def inject_globals():
return {
"app_name": "Gold Monitor",
"public_base": Config.PUBLIC_BASE_URL,
}
@app.get("/login")
def login():
if session.get("user"):
return redirect(url_for("dashboard"))
return render_template("login.html")
@app.post("/login")
def login_post():
user = (request.form.get("username") or "").strip()
pwd = request.form.get("password") or ""
if database.verify_admin(user, pwd):
session["user"] = user
return redirect(request.args.get("next") or url_for("dashboard"))
flash("用户名或密码错误", "error")
return render_template("login.html"), 401
@app.get("/logout")
def logout():
session.clear()
return redirect(url_for("login"))
@app.get("/settings")
@login_required
def settings_page():
return render_template("settings.html")
@app.post("/settings/password")
@login_required
def settings_password():
old_pwd = request.form.get("old_password") or ""
new_pwd = request.form.get("new_password") or ""
new_pwd2 = request.form.get("new_password2") or ""
if new_pwd != new_pwd2:
flash("两次输入的新密码不一致", "error")
return redirect(url_for("settings_page"))
try:
database.change_admin_password(old_pwd, new_pwd)
flash("密码已更新,请牢记新密码", "ok")
except ValueError as exc:
flash(str(exc), "error")
return redirect(url_for("settings_page"))
@app.get("/")
@login_required
def dashboard():
with database.get_db() as conn:
accounts = database.list_accounts(conn)
machines = database.list_machines(conn)
trades = database.list_trades(conn, 20)
return render_template(
"dashboard.html",
accounts=accounts,
machines=machines,
trades=trades,
)
# ---------- 账户 / 机器 ----------
@app.get("/accounts")
@login_required
def accounts_page():
with database.get_db() as conn:
items = database.list_accounts(conn)
return render_template("accounts.html", items=items)
@app.post("/accounts")
@login_required
def accounts_create():
name = (request.form.get("name") or "").strip()
if not name:
flash("名称不能为空", "error")
return redirect(url_for("accounts_page"))
with database.get_db() as conn:
conn.execute(
"""INSERT INTO accounts(name, broker, login_id, account_type, note, enabled, created_at)
VALUES(?,?,?,?,?,?,?)""",
(
name,
(request.form.get("broker") or "").strip(),
(request.form.get("login_id") or "").strip(),
(request.form.get("account_type") or "demo").strip(),
(request.form.get("note") or "").strip(),
1,
database.now(),
),
)
flash("账户已添加", "ok")
return redirect(url_for("accounts_page"))
@app.post("/accounts/<int:aid>/note")
@login_required
def accounts_note(aid: int):
note = (request.form.get("note") or "").strip()
with database.get_db() as conn:
conn.execute("UPDATE accounts SET note=? WHERE id=?", (note, aid))
flash("标注已保存", "ok")
return redirect(url_for("accounts_page"))
@app.get("/machines")
@login_required
def machines_page():
with database.get_db() as conn:
items = database.list_machines(conn)
return render_template("machines.html", items=items)
@app.post("/machines")
@login_required
def machines_create():
name = (request.form.get("name") or "").strip() or "新机器"
with database.get_db() as conn:
conn.execute(
"""INSERT INTO machines(name, device_fingerprint, note, enabled, created_at)
VALUES(?,?,?,?,?)""",
(name, "", (request.form.get("note") or "").strip(), 1, database.now()),
)
flash("机器已添加", "ok")
return redirect(url_for("machines_page"))
@app.post("/machines/<int:mid>/note")
@login_required
def machines_note(mid: int):
note = (request.form.get("note") or "").strip()
name = (request.form.get("name") or "").strip()
with database.get_db() as conn:
if name:
conn.execute("UPDATE machines SET note=?, name=? WHERE id=?", (note, name, mid))
else:
conn.execute("UPDATE machines SET note=? WHERE id=?", (note, mid))
flash("机器标注已保存", "ok")
return redirect(url_for("machines_page"))
# ---------- 下载 / 激活 ----------
@app.get("/download")
@login_required
def download_page():
with database.get_db() as conn:
accounts = database.list_accounts(conn)
machines = database.list_machines(conn)
acts = conn.execute(
"""
SELECT act.*, a.name AS account_name, m.name AS machine_name
FROM activations act
LEFT JOIN accounts a ON a.id = act.account_id
LEFT JOIN machines m ON m.id = act.machine_id
ORDER BY act.id DESC LIMIT 50
"""
).fetchall()
activations = [database.row_to_dict(r) for r in acts]
exe_name = "黄金市价辅助.exe"
exe_path = RELEASES_DIR / exe_name
return render_template(
"download.html",
accounts=accounts,
machines=machines,
activations=activations,
exe_ready=exe_path.is_file(),
exe_name=exe_name,
)
@app.post("/download/activation")
@login_required
def download_create_activation():
account_id = int(request.form.get("account_id") or 0)
machine_raw = request.form.get("machine_id") or ""
machine_id = int(machine_raw) if machine_raw.strip().isdigit() else None
with database.get_db() as conn:
if not conn.execute("SELECT id FROM accounts WHERE id=?", (account_id,)).fetchone():
flash("账户不存在", "error")
return redirect(url_for("download_page"))
code = database.create_activation(conn, account_id, machine_id)
flash(f"已生成激活码:{code}", "ok")
return redirect(url_for("download_page"))
@app.get("/download/file")
@login_required
def download_file():
name = "黄金市价辅助.exe"
path = RELEASES_DIR / name
if not path.is_file():
# 提供占位说明文件,避免空目录无下载
readme = RELEASES_DIR / "README_PLACE_EXE_HERE.txt"
if not readme.is_file():
readme.write_text(
"请将打包好的通用辅助 exe 放到本目录,命名为:黄金市价辅助.exe\n"
"客户端首次启动使用网页生成的激活码绑定机器与账户。\n",
encoding="utf-8",
)
return send_from_directory(RELEASES_DIR, readme.name, as_attachment=True)
return send_from_directory(RELEASES_DIR, name, as_attachment=True)
@app.post("/api/v1/activate")
def api_activate():
data = request.get_json(silent=True) or {}
code = (data.get("code") or "").strip()
fingerprint = (data.get("fingerprint") or secrets.token_hex(8)).strip()
machine_name = (data.get("machine_name") or "").strip()
if not code:
return jsonify({"ok": False, "error": "缺少激活码"}), 400
try:
with database.get_db() as conn:
result = database.activate_device(conn, code, fingerprint, machine_name)
return jsonify({"ok": True, **result})
except ValueError as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
@app.get("/api/v1/ping")
def api_ping():
token = request.headers.get("X-Device-Token") or request.args.get("token") or ""
with database.get_db() as conn:
act = database.auth_token(conn, token)
if not act:
return jsonify({"ok": False, "error": "未激活"}), 401
if act.get("machine_id"):
conn.execute(
"UPDATE machines SET last_seen_at=? WHERE id=?",
(database.now(), act["machine_id"]),
)
return jsonify({"ok": True, "server_time": database.now()})
# ---------- 成交上报 / 复盘 ----------
@app.post("/api/v1/trades")
def api_trades_upsert():
token = request.headers.get("X-Device-Token") or ""
data = request.get_json(silent=True) or {}
with database.get_db() as conn:
act = database.auth_token(conn, token)
if not act:
return jsonify({"ok": False, "error": "未激活"}), 401
snapshot = data.get("snapshot") or {}
conn.execute(
"""
INSERT INTO trades(
ticket, symbol, side, volume, open_price, close_price,
open_time, close_time, profit, sl, tp,
account_id, machine_id, entry_style, review_note, snapshot_json, created_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
str(data.get("ticket") or ""),
(data.get("symbol") or "XAUUSD").upper(),
(data.get("side") or "").lower(),
float(data.get("volume") or 0),
float(data.get("open_price") or 0),
float(data.get("close_price") or 0) if data.get("close_price") is not None else None,
data.get("open_time"),
data.get("close_time"),
float(data.get("profit") or 0) if data.get("profit") is not None else None,
data.get("sl"),
data.get("tp"),
act["account_id"],
act["machine_id"],
(data.get("entry_style") or "").strip(),
(data.get("review_note") or "").strip(),
json.dumps(snapshot, ensure_ascii=False),
database.now(),
),
)
tid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
return jsonify({"ok": True, "id": tid})
@app.post("/api/v1/bars")
def api_bars_upload():
token = request.headers.get("X-Device-Token") or ""
data = request.get_json(silent=True) or {}
with database.get_db() as conn:
if not database.auth_token(conn, token):
return jsonify({"ok": False, "error": "未激活"}), 401
symbol = (data.get("symbol") or "XAUUSD").upper()
timeframe = (data.get("timeframe") or "M1").upper()
bars = data.get("bars") or []
n = database.upsert_bars(conn, symbol, timeframe, bars)
return jsonify({"ok": True, "count": n})
@app.get("/api/v1/bars")
@login_required
def api_bars_get():
symbol = (request.args.get("symbol") or "XAUUSD").upper()
timeframe = (request.args.get("timeframe") or "M15").upper()
start_ts = float(request.args.get("start") or 0)
end_ts = float(request.args.get("end") or database.now())
with database.get_db() as conn:
bars = database.get_bars(conn, symbol, timeframe, start_ts, end_ts)
return jsonify({"ok": True, "bars": bars})
@app.get("/records")
@login_required
def records_page():
with database.get_db() as conn:
trades = database.list_trades(conn, 300)
return render_template("records.html", trades=trades)
@app.get("/records/<int:tid>")
@login_required
def record_detail(tid: int):
with database.get_db() as conn:
trade = database.get_trade(conn, tid)
if not trade:
flash("记录不存在", "error")
return redirect(url_for("records_page"))
# 四周期:以开仓时间为中心取前后窗口
center = trade.get("open_time") or trade.get("created_at") or database.now()
window = 3600 * 6
charts = {}
for tf in ("M1", "M5", "M15", "H1"):
charts[tf] = database.get_bars(
conn, trade["symbol"], tf, float(center) - window, float(center) + window
)
return render_template("record_detail.html", trade=trade, charts=charts)
@app.post("/records/<int:tid>/annotate")
@login_required
def record_annotate(tid: int):
entry_style = (request.form.get("entry_style") or "").strip()
note = (request.form.get("review_note") or "").strip()
with database.get_db() as conn:
conn.execute(
"UPDATE trades SET entry_style=?, review_note=? WHERE id=?",
(entry_style, note, tid),
)
flash("复盘标注已保存", "ok")
return redirect(url_for("record_detail", tid=tid))
@app.get("/health")
def health():
return jsonify({"ok": True, "service": "gold_monitor", "port": Config.PORT})