rebuild(stage-12): deliver private review workflows
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
|
||||
class ReviewRepository:
|
||||
def watchlist(self, connection: sqlite3.Connection, user_id: int) -> tuple[sqlite3.Row, ...]:
|
||||
return tuple(
|
||||
connection.execute(
|
||||
"""SELECT identifier, name, sector, remark, created_at FROM watchlist_entries
|
||||
WHERE user_id = ? ORDER BY created_at, identifier""",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
)
|
||||
|
||||
def save_watch(
|
||||
self, connection: sqlite3.Connection, user_id: int, item: dict, now: str
|
||||
) -> None:
|
||||
connection.execute(
|
||||
"""INSERT INTO watchlist_entries (user_id, identifier, name, sector, remark, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, identifier) DO UPDATE SET
|
||||
name=excluded.name, sector=excluded.sector,
|
||||
remark=CASE
|
||||
WHEN excluded.remark = '' THEN watchlist_entries.remark
|
||||
ELSE excluded.remark
|
||||
END""",
|
||||
(
|
||||
user_id,
|
||||
item["identifier"],
|
||||
item["name"],
|
||||
item.get("sector"),
|
||||
item.get("remark") or "",
|
||||
now,
|
||||
),
|
||||
)
|
||||
|
||||
def delete_watch(self, connection: sqlite3.Connection, user_id: int, identifier: str) -> bool:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM watchlist_entries WHERE user_id = ? AND identifier = ?",
|
||||
(user_id, identifier),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def save_watch_remark(
|
||||
self, connection: sqlite3.Connection, user_id: int, identifier: str, remark: str
|
||||
) -> bool:
|
||||
cursor = connection.execute(
|
||||
"UPDATE watchlist_entries SET remark=? WHERE user_id=? AND identifier=?",
|
||||
(remark, user_id, identifier),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def latest_factors(self, connection: sqlite3.Connection, through: str) -> dict[str, dict]:
|
||||
snapshot = connection.execute(
|
||||
"""SELECT id, trade_date FROM screener_factor_snapshots
|
||||
WHERE trade_date <= ? ORDER BY trade_date DESC, id DESC LIMIT 1""",
|
||||
(through,),
|
||||
).fetchone()
|
||||
if snapshot is None:
|
||||
return {}
|
||||
return {
|
||||
str(row["identifier"]): json.loads(str(row["payload_json"]))
|
||||
for row in connection.execute(
|
||||
"SELECT identifier, payload_json FROM screener_factor_values WHERE snapshot_id = ?",
|
||||
(int(snapshot["id"]),),
|
||||
).fetchall()
|
||||
}
|
||||
|
||||
def latest_auction_scores(
|
||||
self, connection: sqlite3.Connection, through: str
|
||||
) -> dict[str, float]:
|
||||
row = connection.execute(
|
||||
"""SELECT payload_json FROM market_insight_snapshots
|
||||
WHERE kind = 'auction' AND trade_date <= ? AND entity_key = ''
|
||||
ORDER BY trade_date DESC LIMIT 1""",
|
||||
(through,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return {}
|
||||
payload = json.loads(str(row["payload_json"]))
|
||||
scores = {}
|
||||
for key in ("_market_rows", "rows", "one_price_rows"):
|
||||
for item in payload.get(key) or []:
|
||||
identifier = str(item.get("identifier") or item.get("ts_code") or "")
|
||||
score = item.get("attention_score")
|
||||
if identifier and isinstance(score, (int, float)):
|
||||
scores[identifier] = float(score)
|
||||
return scores
|
||||
|
||||
def note(
|
||||
self, connection: sqlite3.Connection, user_id: int, code: str, trade_date: str
|
||||
) -> sqlite3.Row | None:
|
||||
return connection.execute(
|
||||
"SELECT * FROM review_notes WHERE user_id = ? AND code = ? AND trade_date = ?",
|
||||
(user_id, code, trade_date),
|
||||
).fetchone()
|
||||
|
||||
def notes(
|
||||
self, connection: sqlite3.Connection, user_id: int, code: str = "", limit: int = 60
|
||||
) -> tuple[sqlite3.Row, ...]:
|
||||
return tuple(
|
||||
connection.execute(
|
||||
"""SELECT * FROM review_notes WHERE user_id = ? AND code = ?
|
||||
ORDER BY trade_date DESC, id DESC LIMIT ?""",
|
||||
(user_id, code, limit),
|
||||
).fetchall()
|
||||
)
|
||||
|
||||
def save_note(self, connection: sqlite3.Connection, user_id: int, data: dict, now: str) -> int:
|
||||
connection.execute(
|
||||
"""INSERT INTO review_notes (
|
||||
user_id, code, stock_name, trade_date, summary, content, plan,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, code, trade_date) DO UPDATE SET
|
||||
stock_name=excluded.stock_name, summary=excluded.summary,
|
||||
content=excluded.content, plan=excluded.plan, updated_at=excluded.updated_at""",
|
||||
(
|
||||
user_id,
|
||||
data["code"],
|
||||
data["stock_name"],
|
||||
data["trade_date"],
|
||||
data["summary"],
|
||||
data["content"],
|
||||
data["plan"],
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
row = self.note(connection, user_id, data["code"], data["trade_date"])
|
||||
if row is None:
|
||||
raise RuntimeError("复盘记录写入失败")
|
||||
return int(row["id"])
|
||||
|
||||
def delete_note(self, connection: sqlite3.Connection, user_id: int, note_id: int) -> bool:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM review_notes WHERE user_id = ? AND id = ?", (user_id, note_id)
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def trades(
|
||||
self, connection: sqlite3.Connection, user_id: int, limit: int = 300
|
||||
) -> tuple[sqlite3.Row, ...]:
|
||||
return tuple(
|
||||
connection.execute(
|
||||
"""SELECT * FROM trade_entries WHERE user_id = ?
|
||||
ORDER BY trade_date DESC, id DESC LIMIT ?""",
|
||||
(user_id, limit),
|
||||
).fetchall()
|
||||
)
|
||||
|
||||
def trade(
|
||||
self, connection: sqlite3.Connection, user_id: int, trade_id: int
|
||||
) -> sqlite3.Row | None:
|
||||
return connection.execute(
|
||||
"SELECT * FROM trade_entries WHERE user_id = ? AND id = ?", (user_id, trade_id)
|
||||
).fetchone()
|
||||
|
||||
def save_trade(self, connection: sqlite3.Connection, user_id: int, data: dict, now: str) -> int:
|
||||
values = (
|
||||
data["trade_date"],
|
||||
data["code"],
|
||||
data["name"],
|
||||
data["action"],
|
||||
data["price"],
|
||||
data["quantity"],
|
||||
data["position_pct"],
|
||||
data["pnl_amount"],
|
||||
data["pnl_pct"],
|
||||
data["emotion"],
|
||||
json.dumps(data["tags"], ensure_ascii=False),
|
||||
data["thesis"],
|
||||
data["execution"],
|
||||
now,
|
||||
)
|
||||
if data.get("id"):
|
||||
cursor = connection.execute(
|
||||
"""UPDATE trade_entries SET trade_date=?, code=?, name=?, action=?, price=?,
|
||||
quantity=?, position_pct=?, pnl_amount=?, pnl_pct=?, emotion=?, tags_json=?,
|
||||
thesis=?, execution=?, updated_at=? WHERE user_id=? AND id=?""",
|
||||
(*values, user_id, int(data["id"])),
|
||||
)
|
||||
if cursor.rowcount == 0:
|
||||
raise ValueError("交易记录不存在或无权修改。")
|
||||
return int(data["id"])
|
||||
cursor = connection.execute(
|
||||
"""INSERT INTO trade_entries (
|
||||
trade_date, code, name, action, price, quantity, position_pct, pnl_amount,
|
||||
pnl_pct, emotion, tags_json, thesis, execution, created_at, updated_at, user_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(*values, now, user_id),
|
||||
)
|
||||
return int(cursor.lastrowid)
|
||||
|
||||
def delete_trade(self, connection: sqlite3.Connection, user_id: int, trade_id: int) -> bool:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM trade_entries WHERE user_id = ? AND id = ?", (user_id, trade_id)
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def save_alert(self, connection: sqlite3.Connection, user_id: int, data: dict, now: str) -> int:
|
||||
connection.execute(
|
||||
"""INSERT INTO alerts (
|
||||
user_id, kind, title, content, available_date, code, dedupe_key,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, dedupe_key) DO NOTHING""",
|
||||
(
|
||||
user_id,
|
||||
data["kind"],
|
||||
data["title"],
|
||||
data["content"],
|
||||
data["available_date"],
|
||||
data["code"],
|
||||
data["dedupe_key"],
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
row = connection.execute(
|
||||
"SELECT id FROM alerts WHERE user_id = ? AND dedupe_key = ?",
|
||||
(user_id, data["dedupe_key"]),
|
||||
).fetchone()
|
||||
return int(row["id"])
|
||||
|
||||
def alerts(
|
||||
self, connection: sqlite3.Connection, user_id: int, unread: bool
|
||||
) -> tuple[sqlite3.Row, ...]:
|
||||
clause = "AND is_read = 0" if unread else ""
|
||||
return tuple(
|
||||
connection.execute(
|
||||
f"""SELECT * FROM alerts WHERE user_id = ? {clause}
|
||||
ORDER BY available_date > date('now'), is_read,
|
||||
available_date DESC, id DESC LIMIT 100""",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
)
|
||||
|
||||
def unread_count(self, connection: sqlite3.Connection, user_id: int, today: str) -> int:
|
||||
row = connection.execute(
|
||||
"""SELECT COUNT(*) total FROM alerts
|
||||
WHERE user_id = ? AND available_date <= ? AND is_read = 0""",
|
||||
(user_id, today),
|
||||
).fetchone()
|
||||
return int(row["total"])
|
||||
|
||||
def mark_alert(
|
||||
self, connection: sqlite3.Connection, user_id: int, alert_id: int, now: str
|
||||
) -> bool:
|
||||
cursor = connection.execute(
|
||||
"UPDATE alerts SET is_read=1, read_at=?, updated_at=? WHERE user_id=? AND id=?",
|
||||
(now, now, user_id, alert_id),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def mark_all_alerts(
|
||||
self, connection: sqlite3.Connection, user_id: int, today: str, now: str
|
||||
) -> int:
|
||||
cursor = connection.execute(
|
||||
"""UPDATE alerts SET is_read=1, read_at=?, updated_at=?
|
||||
WHERE user_id=? AND available_date <= ? AND is_read=0""",
|
||||
(now, now, user_id, today),
|
||||
)
|
||||
return int(cursor.rowcount)
|
||||
|
||||
def delete_alert(self, connection: sqlite3.Connection, user_id: int, alert_id: int) -> bool:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM alerts WHERE user_id=? AND id=?", (user_id, alert_id)
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def messages(
|
||||
self, connection: sqlite3.Connection, user_id: int, limit: int = 100
|
||||
) -> tuple[sqlite3.Row, ...]:
|
||||
rows = connection.execute(
|
||||
"""SELECT * FROM review_assistant_messages WHERE user_id=?
|
||||
ORDER BY id DESC LIMIT ?""",
|
||||
(user_id, limit),
|
||||
).fetchall()
|
||||
return tuple(reversed(rows))
|
||||
|
||||
def add_message(
|
||||
self, connection: sqlite3.Connection, user_id: int, data: dict, now: str
|
||||
) -> None:
|
||||
connection.execute(
|
||||
"""INSERT INTO review_assistant_messages
|
||||
(user_id, role, content, context_date, request_id, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
user_id,
|
||||
data["role"],
|
||||
data["content"],
|
||||
data["context_date"],
|
||||
data.get("request_id"),
|
||||
data["status"],
|
||||
now,
|
||||
),
|
||||
)
|
||||
|
||||
def clear_messages(self, connection: sqlite3.Connection, user_id: int) -> int:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM review_assistant_messages WHERE user_id=?", (user_id,)
|
||||
)
|
||||
return int(cursor.rowcount)
|
||||
Reference in New Issue
Block a user