Restructure into modules/ with single-process CTP and config/ layout.
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
|
||||
from modules.records.routes import register
|
||||
|
||||
__all__ = ["register"]
|
||||
@@ -0,0 +1,554 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
"""HTTP routes for records module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from flask import (
|
||||
Response,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_file,
|
||||
session,
|
||||
stream_with_context,
|
||||
url_for,
|
||||
)
|
||||
|
||||
|
||||
def register(deps) -> None:
|
||||
app = deps.app
|
||||
login_required = deps.login_required
|
||||
require_nav = deps.require_nav
|
||||
get_db = deps.get_db
|
||||
get_setting = deps.get_setting
|
||||
set_setting = deps.set_setting
|
||||
fetch_price = deps.fetch_price
|
||||
send_wechat_msg = deps.send_wechat_msg
|
||||
touch_stats_cache = deps.touch_stats_cache
|
||||
get_stats_data = deps.get_stats_data
|
||||
build_market_quote_payload = deps.build_market_quote_payload
|
||||
today_str = deps.today_str
|
||||
expire_old_plans = deps.expire_old_plans
|
||||
TZ = deps.tz
|
||||
DB_PATH = deps.db_path
|
||||
UPLOAD_DIR = deps.upload_dir
|
||||
OPEN_TYPES = deps.open_types
|
||||
EXIT_TRIGGERS = deps.exit_triggers
|
||||
BEHAVIOR_TAGS = deps.behavior_tags
|
||||
KLINE_PERIODS = deps.kline_periods
|
||||
KLINE_CUTOFFS = deps.kline_cutoffs
|
||||
calc_holding_duration = deps.calc_holding_duration
|
||||
holding_to_minutes = deps.holding_to_minutes
|
||||
classify_close_result = deps.classify_close_result
|
||||
calc_rr_ratio = deps.calc_rr_ratio
|
||||
calc_theoretical_pnl = deps.calc_theoretical_pnl
|
||||
parse_review_date_filter = deps.parse_review_date_filter
|
||||
_trading_mode = deps.trading_mode
|
||||
_ua_is_phone = deps.ua_is_phone
|
||||
_static_asset_v = deps.static_asset_v
|
||||
|
||||
from werkzeug.utils import secure_filename
|
||||
from modules.core.contract_specs import calc_position_metrics
|
||||
from modules.fees.fee_specs import calc_fee_breakdown, calc_round_trip_fee
|
||||
from modules.market.kline_chart import generate_review_kline_chart
|
||||
|
||||
@app.route("/api/position_live")
|
||||
@login_required
|
||||
def api_position_live():
|
||||
capital = float(get_setting("live_capital", "0") or 0)
|
||||
now_iso = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M")
|
||||
conn = get_db()
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
out = []
|
||||
for r in rows:
|
||||
sym = r["symbol"]
|
||||
market = r["market_code"] or ""
|
||||
sina = r["sina_code"] or ""
|
||||
direction = r["direction"]
|
||||
entry = float(r["entry_price"])
|
||||
sl = float(r["stop_loss"])
|
||||
tp = float(r["take_profit"])
|
||||
lots = float(r["lots"] or 1)
|
||||
mark = fetch_price(sym, market, sina)
|
||||
metrics = calc_position_metrics(
|
||||
direction, entry, sl, tp, lots, mark, capital, sym,
|
||||
)
|
||||
holding = calc_holding_duration(r["open_time"] or "", now_iso)
|
||||
close_est = mark if mark is not None else entry
|
||||
fee_info = calc_fee_breakdown(
|
||||
sym, entry, close_est, lots, r["open_time"] or "", now_iso,
|
||||
trading_mode=_trading_mode(),
|
||||
)
|
||||
est_net = None
|
||||
if metrics.get("float_pnl") is not None:
|
||||
est_net = round(metrics["float_pnl"] - fee_info["total_fee"], 2)
|
||||
out.append({
|
||||
"id": r["id"],
|
||||
"symbol": r["symbol_name"] or sym,
|
||||
"symbol_code": sym,
|
||||
"direction": "做多" if direction == "long" else "做空",
|
||||
"lots": lots,
|
||||
"entry_price": entry,
|
||||
"stop_loss": sl,
|
||||
"take_profit": tp,
|
||||
"open_time": r["open_time"],
|
||||
"mark_price": mark,
|
||||
"holding_duration": holding,
|
||||
"est_fee": fee_info["total_fee"],
|
||||
"est_fee_open": fee_info["open_fee"],
|
||||
"est_fee_close": fee_info["close_fee"],
|
||||
"est_fee_close_type": fee_info["close_type"],
|
||||
"est_pnl_net": est_net,
|
||||
**metrics,
|
||||
})
|
||||
return jsonify(out)
|
||||
@app.route("/close_position/<int:pid>", methods=["POST"])
|
||||
@login_required
|
||||
def close_position(pid):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT * FROM position_monitors WHERE id=?", (pid,)).fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
flash("持仓不存在")
|
||||
return redirect(url_for("positions"))
|
||||
sym = row["symbol"]
|
||||
market = row["market_code"] or ""
|
||||
sina = row["sina_code"] or ""
|
||||
direction = row["direction"]
|
||||
entry = float(row["entry_price"])
|
||||
sl = float(row["stop_loss"])
|
||||
tp = float(row["take_profit"])
|
||||
lots = float(row["lots"] or 1)
|
||||
open_time = row["open_time"] or ""
|
||||
close_time = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M")
|
||||
close_price = fetch_price(sym, market, sina)
|
||||
if close_price is None:
|
||||
conn.close()
|
||||
flash("无法获取现价,平仓失败")
|
||||
return redirect(url_for("positions"))
|
||||
capital = float(get_setting("live_capital", "0") or 0)
|
||||
metrics = calc_position_metrics(direction, entry, sl, tp, lots, close_price, capital, sym)
|
||||
pnl = metrics.get("float_pnl") or 0.0
|
||||
fee = calc_round_trip_fee(sym, entry, close_price, lots, open_time, close_time, trading_mode=_trading_mode())
|
||||
pnl_net = round(pnl - fee, 2)
|
||||
result = classify_close_result(direction, close_price, sl, tp)
|
||||
minutes = holding_to_minutes(open_time, close_time)
|
||||
margin_pct = metrics.get("position_pct")
|
||||
from modules.trading.trade_log_lib import calc_equity_after, refresh_trade_log_equity_chain
|
||||
equity_after = calc_equity_after(capital, pnl_net)
|
||||
conn.execute(
|
||||
"""INSERT INTO trade_logs
|
||||
(symbol, symbol_name, market_code, sina_code, monitor_type, direction,
|
||||
entry_price, stop_loss, take_profit, close_price, lots, margin,
|
||||
margin_pct, holding_minutes, open_time, close_time, pnl, fee, pnl_net,
|
||||
equity_after, result)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
sym, row["symbol_name"], market, sina, "持仓监控", direction,
|
||||
entry, sl, tp, close_price, lots, metrics["margin"],
|
||||
margin_pct,
|
||||
minutes, open_time, close_time, pnl, fee, pnl_net, equity_after, result,
|
||||
),
|
||||
)
|
||||
conn.execute("DELETE FROM position_monitors WHERE id=?", (pid,))
|
||||
try:
|
||||
refresh_trade_log_equity_chain(conn, capital if capital > 0 else None)
|
||||
except Exception as exc:
|
||||
app.logger.debug("equity chain refresh after close: %s", exc)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
touch_stats_cache()
|
||||
flash(f"已平仓,盈亏 {pnl:.2f} 元(扣费后 {pnl_net:.2f} 元),已记入交易记录")
|
||||
return redirect(url_for("positions"))
|
||||
|
||||
|
||||
@app.route("/trades")
|
||||
@login_required
|
||||
def trades():
|
||||
return redirect(url_for("records"))
|
||||
|
||||
|
||||
@app.route("/update_trade/<int:tid>", methods=["POST"])
|
||||
@login_required
|
||||
def update_trade(tid):
|
||||
d = request.form
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT * FROM trade_logs WHERE id=?", (tid,)).fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
flash("记录不存在")
|
||||
return redirect(url_for("records"))
|
||||
row = dict(row)
|
||||
entry = float(d.get("entry_price") or 0)
|
||||
close_px = float(d.get("close_price") or 0)
|
||||
lots = float(d.get("lots") or 0)
|
||||
sl_raw = d.get("stop_loss")
|
||||
tp_raw = d.get("take_profit")
|
||||
stop_loss = float(sl_raw) if sl_raw not in (None, "") else None
|
||||
take_profit = float(tp_raw) if tp_raw not in (None, "") else None
|
||||
open_time = (d.get("open_time") or row.get("open_time") or "").strip()
|
||||
close_time = (d.get("close_time") or row.get("close_time") or "").strip()
|
||||
direction = (d.get("direction") or row.get("direction") or "long").strip()
|
||||
|
||||
from modules.trading.trade_log_lib import recalc_trade_log_pnl, refresh_trade_log_equity_chain, _read_initial_capital
|
||||
from modules.core.trading_context import get_trading_mode
|
||||
|
||||
pnl = float(row.get("pnl") or 0)
|
||||
fee = float(row.get("fee") or 0)
|
||||
pnl_net = float(row.get("pnl_net") or 0)
|
||||
old_entry = float(row.get("entry_price") or 0)
|
||||
old_close = float(row.get("close_price") or 0)
|
||||
old_lots = float(row.get("lots") or 0)
|
||||
prices_changed = (
|
||||
abs(entry - old_entry) > 0.0001
|
||||
or abs(close_px - old_close) > 0.0001
|
||||
or abs(lots - old_lots) > 0.0001
|
||||
)
|
||||
if prices_changed and close_px > 0 and entry > 0 and lots > 0:
|
||||
calc = recalc_trade_log_pnl(
|
||||
symbol=row.get("symbol") or "",
|
||||
direction=direction,
|
||||
entry_price=entry,
|
||||
close_price=close_px,
|
||||
lots=lots,
|
||||
stop_loss=stop_loss,
|
||||
take_profit=take_profit,
|
||||
open_time=open_time,
|
||||
close_time=close_time,
|
||||
trading_mode=get_trading_mode(get_setting),
|
||||
)
|
||||
pnl = calc["pnl"]
|
||||
fee = calc["fee"]
|
||||
pnl_net = calc["pnl_net"]
|
||||
|
||||
form_pnl_raw = d.get("pnl")
|
||||
if form_pnl_raw not in (None, ""):
|
||||
pnl = float(form_pnl_raw)
|
||||
pnl_net = round(pnl - fee, 2)
|
||||
|
||||
try:
|
||||
holding_to_minutes = deps.holding_to_minutes
|
||||
minutes = int(holding_to_minutes(open_time, close_time) or 0)
|
||||
except Exception:
|
||||
minutes = int(d.get("holding_minutes") or row.get("holding_minutes") or 0)
|
||||
|
||||
conn.execute(
|
||||
"""UPDATE trade_logs SET
|
||||
symbol_name=?, monitor_type=?, direction=?,
|
||||
entry_price=?, stop_loss=?, take_profit=?, close_price=?,
|
||||
lots=?, margin=?, holding_minutes=?, open_time=?, close_time=?,
|
||||
pnl=?, fee=?, pnl_net=?, result=?, verified=1
|
||||
WHERE id=?""",
|
||||
(
|
||||
d.get("symbol_name", "").strip(),
|
||||
d.get("monitor_type", "").strip(),
|
||||
direction,
|
||||
entry,
|
||||
stop_loss,
|
||||
take_profit,
|
||||
close_px,
|
||||
lots,
|
||||
float(d.get("margin") or 0),
|
||||
minutes,
|
||||
open_time,
|
||||
close_time,
|
||||
pnl,
|
||||
fee,
|
||||
pnl_net,
|
||||
d.get("result", "").strip(),
|
||||
tid,
|
||||
),
|
||||
)
|
||||
try:
|
||||
refresh_trade_log_equity_chain(conn, _read_initial_capital(conn))
|
||||
except Exception as exc:
|
||||
app.logger.debug("equity chain refresh after trade edit: %s", exc)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
touch_stats_cache()
|
||||
flash("交易记录已核对保存")
|
||||
return redirect(url_for("records"))
|
||||
|
||||
|
||||
@app.route("/del_trade/<int:tid>")
|
||||
@login_required
|
||||
def del_trade(tid):
|
||||
conn = get_db()
|
||||
conn.execute("DELETE FROM trade_logs WHERE id=?", (tid,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
touch_stats_cache()
|
||||
flash("已删除")
|
||||
return redirect(url_for("records"))
|
||||
|
||||
|
||||
@app.route("/fill_review/<int:tid>")
|
||||
@login_required
|
||||
def fill_review_from_trade(tid):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT * FROM trade_logs WHERE id=?", (tid,)).fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
flash("记录不存在")
|
||||
return redirect(url_for("records"))
|
||||
q = {
|
||||
"symbol": row["symbol"],
|
||||
"symbol_name": row["symbol_name"] or row["symbol"],
|
||||
"market_code": row["market_code"] or "",
|
||||
"sina_code": row["sina_code"] or "",
|
||||
"direction": row["direction"],
|
||||
"entry_price": row["entry_price"],
|
||||
"stop_loss": row["stop_loss"],
|
||||
"take_profit": row["take_profit"],
|
||||
"close_price": row["close_price"],
|
||||
"lots": row["lots"],
|
||||
"open_time": row["open_time"],
|
||||
"close_time": row["close_time"],
|
||||
"pnl": row["pnl"],
|
||||
}
|
||||
params = {k: v for k, v in q.items() if v is not None}
|
||||
return redirect(url_for("records", **params) + "#review-panel")
|
||||
|
||||
|
||||
@app.route("/records")
|
||||
@login_required
|
||||
def records():
|
||||
preset = request.args.get("preset", "")
|
||||
start = request.args.get("start", "")
|
||||
end = request.args.get("end", "")
|
||||
if preset:
|
||||
start, end = parse_review_date_filter(preset, start, end)
|
||||
|
||||
conn = get_db()
|
||||
ctp_sync_info = None
|
||||
sql = "SELECT * FROM review_records WHERE 1=1"
|
||||
params: list = []
|
||||
if start:
|
||||
sql += " AND date(close_time) >= ?"
|
||||
params.append(start)
|
||||
if end:
|
||||
sql += " AND date(close_time) <= ?"
|
||||
params.append(end)
|
||||
sql += " ORDER BY id DESC LIMIT 200"
|
||||
review_list = conn.execute(sql, params).fetchall()
|
||||
|
||||
auto_list = conn.execute(
|
||||
"SELECT * FROM trade_records ORDER BY id DESC LIMIT 30"
|
||||
).fetchall()
|
||||
trade_list = conn.execute(
|
||||
"SELECT * FROM trade_logs ORDER BY id DESC LIMIT 500"
|
||||
).fetchall()
|
||||
from modules.trading.trade_log_lib import enrich_trades_for_records, _read_initial_capital
|
||||
try:
|
||||
initial_capital = _read_initial_capital(conn)
|
||||
except Exception:
|
||||
initial_capital = 100_000.0
|
||||
trades, equity_curve = enrich_trades_for_records(
|
||||
[dict(r) for r in trade_list],
|
||||
initial_capital=initial_capital,
|
||||
)
|
||||
conn.close()
|
||||
|
||||
trade_prefill_keys = (
|
||||
"symbol", "symbol_name", "market_code", "sina_code", "direction",
|
||||
"entry_price", "stop_loss", "take_profit", "close_price",
|
||||
"lots", "open_time", "close_time", "pnl",
|
||||
)
|
||||
prefill = {k: request.args.get(k) for k in trade_prefill_keys if request.args.get(k)}
|
||||
|
||||
return render_template(
|
||||
"records.html",
|
||||
reviews=review_list,
|
||||
trades=trades,
|
||||
equity_curve=equity_curve,
|
||||
auto_records=auto_list,
|
||||
ctp_sync_info=ctp_sync_info,
|
||||
preset=preset,
|
||||
start=start,
|
||||
end=end,
|
||||
prefill=prefill,
|
||||
open_types=OPEN_TYPES,
|
||||
exit_triggers=EXIT_TRIGGERS,
|
||||
behavior_tags=BEHAVIOR_TAGS,
|
||||
kline_periods=KLINE_PERIODS,
|
||||
kline_cutoffs=KLINE_CUTOFFS,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/add_review", methods=["POST"])
|
||||
@login_required
|
||||
def add_review():
|
||||
d = request.form
|
||||
open_type = d.get("open_type", "").strip()
|
||||
exit_trigger = d.get("exit_trigger", "").strip()
|
||||
if not open_type:
|
||||
flash("请选择开仓类型")
|
||||
return redirect(url_for("records"))
|
||||
if not exit_trigger:
|
||||
flash("请选择离场触发")
|
||||
return redirect(url_for("records"))
|
||||
|
||||
symbol = d.get("symbol", "").strip()
|
||||
symbol_name = d.get("symbol_name", "").strip()
|
||||
market_code = d.get("market_code", "").strip()
|
||||
sina_code = d.get("sina_code", "").strip()
|
||||
if not symbol or not market_code:
|
||||
flash("请从下拉列表选择品种(同花顺合约代码)")
|
||||
return redirect(url_for("records"))
|
||||
|
||||
screenshot = ""
|
||||
f = request.files.get("screenshot")
|
||||
if f and f.filename:
|
||||
fname = secure_filename(f.filename)
|
||||
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
|
||||
screenshot = f"{ts}_{fname}"
|
||||
f.save(os.path.join(UPLOAD_DIR, screenshot))
|
||||
|
||||
tags = [t for t in BEHAVIOR_TAGS if d.get(f"tag_{t}")]
|
||||
is_emotion = 1 if tags else 0
|
||||
|
||||
def num(key: str) -> Optional[float]:
|
||||
v = d.get(key, "").strip()
|
||||
if not v:
|
||||
return None
|
||||
return float(v)
|
||||
|
||||
open_time = d.get("open_time", "").strip()
|
||||
close_time = d.get("close_time", "").strip()
|
||||
direction = d.get("direction", "").strip()
|
||||
entry_price = num("entry_price")
|
||||
stop_loss = num("stop_loss")
|
||||
take_profit = num("take_profit")
|
||||
close_price = num("close_price")
|
||||
lots = num("lots") or 1.0
|
||||
|
||||
holding = calc_holding_duration(open_time, close_time)
|
||||
initial_pnl = calc_rr_ratio(direction, entry_price, stop_loss, take_profit)
|
||||
actual_pnl = calc_rr_ratio(direction, entry_price, stop_loss, close_price)
|
||||
|
||||
gross_pnl = num("pnl")
|
||||
if gross_pnl is None and entry_price and close_price:
|
||||
spec_mult = calc_position_metrics(
|
||||
direction, entry_price, stop_loss, take_profit,
|
||||
lots, close_price, 0, symbol,
|
||||
)
|
||||
gross_pnl = spec_mult.get("float_pnl")
|
||||
fee = calc_round_trip_fee(
|
||||
symbol, entry_price or 0, close_price or 0, lots, open_time, close_time,
|
||||
trading_mode=_trading_mode(),
|
||||
)
|
||||
pnl_net = round((gross_pnl or 0) - fee, 2) if gross_pnl is not None else None
|
||||
|
||||
auto_kline = bool(d.get("auto_kline"))
|
||||
if auto_kline and not screenshot:
|
||||
try:
|
||||
generated = generate_review_kline_chart(
|
||||
symbol=symbol,
|
||||
periods=[d.get("kline_period1", "15m"), d.get("kline_period2", "1h")],
|
||||
count=int(d.get("kline_count") or 300),
|
||||
cutoff_label=d.get("kline_cutoff", "平仓时间"),
|
||||
open_time=open_time,
|
||||
close_time=close_time,
|
||||
entry_price=entry_price,
|
||||
stop_loss=stop_loss,
|
||||
take_profit=take_profit,
|
||||
close_price=close_price,
|
||||
upload_dir=UPLOAD_DIR,
|
||||
)
|
||||
if generated:
|
||||
screenshot = generated
|
||||
except Exception as exc:
|
||||
app.logger.warning("auto kline failed: %s", exc)
|
||||
|
||||
conn = get_db()
|
||||
conn.execute(
|
||||
"""INSERT INTO review_records
|
||||
(open_time, close_time, symbol, symbol_name, market_code, sina_code,
|
||||
timeframe, direction,
|
||||
entry_price, stop_loss, take_profit, close_price, lots,
|
||||
holding_duration, initial_pnl, actual_pnl, pnl, fee, pnl_net,
|
||||
open_type, expected_rr, actual_rr, exit_trigger, exit_supplement,
|
||||
watch_after_breakeven, new_position_while_occupied, screenshot,
|
||||
auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff,
|
||||
behavior_tags, is_emotion, notes)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
open_time, close_time,
|
||||
symbol, symbol_name, market_code, sina_code,
|
||||
d.get("timeframe", "").strip(),
|
||||
direction,
|
||||
entry_price, stop_loss, take_profit, close_price, lots,
|
||||
holding, initial_pnl, actual_pnl, gross_pnl, fee, pnl_net,
|
||||
open_type,
|
||||
None,
|
||||
None,
|
||||
exit_trigger,
|
||||
d.get("exit_supplement", "").strip(),
|
||||
d.get("watch_after_breakeven", "否"),
|
||||
d.get("new_position_while_occupied", "否"),
|
||||
screenshot,
|
||||
1 if auto_kline else 0,
|
||||
d.get("kline_period1", "15m"),
|
||||
d.get("kline_period2", "1h"),
|
||||
int(d.get("kline_count") or 300),
|
||||
d.get("kline_cutoff", "平仓时间"),
|
||||
",".join(tags),
|
||||
is_emotion,
|
||||
d.get("notes", "").strip(),
|
||||
),
|
||||
)
|
||||
hook = getattr(app, "_risk_review_hook", None)
|
||||
if hook:
|
||||
hook(
|
||||
conn,
|
||||
",".join(tags),
|
||||
exit_trigger,
|
||||
d.get("exit_supplement", "").strip(),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
touch_stats_cache()
|
||||
flash("复盘记录已保存")
|
||||
return redirect(url_for("records"))
|
||||
|
||||
|
||||
@app.route("/del_review/<int:rid>")
|
||||
@login_required
|
||||
def del_review(rid):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT screenshot FROM review_records WHERE id=?", (rid,)).fetchone()
|
||||
if row and row["screenshot"]:
|
||||
path = os.path.join(UPLOAD_DIR, row["screenshot"])
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
conn.execute("DELETE FROM review_records WHERE id=?", (rid,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
touch_stats_cache()
|
||||
flash("已删除")
|
||||
return redirect(url_for("records"))
|
||||
|
||||
|
||||
@app.route("/uploads/<path:filename>")
|
||||
@login_required
|
||||
def uploaded_file(filename):
|
||||
from flask import send_from_directory
|
||||
return send_from_directory(UPLOAD_DIR, filename)
|
||||
|
||||
|
||||
@app.route("/del_record/<int:rid>")
|
||||
@login_required
|
||||
def del_record(rid):
|
||||
conn = get_db()
|
||||
conn.execute("DELETE FROM trade_records WHERE id=?", (rid,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash("已删除")
|
||||
return redirect(url_for("records"))
|
||||
Reference in New Issue
Block a user