migration: preserve review journal alerts and assistant slice
This commit is contained in:
+4
-383
@@ -8,6 +8,7 @@ from typing import Any
|
||||
|
||||
from backend.database import MIGRATIONS, MigrationRunner, SQLiteConnectionFactory
|
||||
from backend.features.accounts.repository import AccountRepositoryMixin
|
||||
from backend.features.alerts.repository import AlertRepositoryMixin
|
||||
from backend.features.auction.repository import AuctionRepositoryMixin
|
||||
from backend.features.dragon_tiger.repository import DragonTigerRepositoryMixin
|
||||
from backend.features.heaven.repository import HeavenRepositoryMixin
|
||||
@@ -15,6 +16,7 @@ from backend.features.market.repository import MarketRepositoryMixin
|
||||
from backend.features.mentor.repository import MentorRepositoryMixin
|
||||
from backend.features.pools.repository import PoolRepositoryMixin
|
||||
from backend.features.popularity.repository import PopularityRepositoryMixin
|
||||
from backend.features.review.repository import ReviewRepositoryMixin
|
||||
from backend.features.screener.repository import ScreenerRepositoryMixin
|
||||
from backend.features.system.repository import SystemSettingsRepositoryMixin
|
||||
from backend.llm.repository import LLMAuditRepositoryMixin
|
||||
@@ -22,6 +24,7 @@ from backend.llm.repository import LLMAuditRepositoryMixin
|
||||
|
||||
class ReviewDatabase(
|
||||
AccountRepositoryMixin,
|
||||
AlertRepositoryMixin,
|
||||
AuctionRepositoryMixin,
|
||||
DragonTigerRepositoryMixin,
|
||||
HeavenRepositoryMixin,
|
||||
@@ -29,6 +32,7 @@ class ReviewDatabase(
|
||||
MentorRepositoryMixin,
|
||||
PoolRepositoryMixin,
|
||||
PopularityRepositoryMixin,
|
||||
ReviewRepositoryMixin,
|
||||
ScreenerRepositoryMixin,
|
||||
SystemSettingsRepositoryMixin,
|
||||
LLMAuditRepositoryMixin,
|
||||
@@ -662,147 +666,6 @@ class ReviewDatabase(
|
||||
MigrationRunner().apply(connection, MIGRATIONS)
|
||||
|
||||
|
||||
def list_watchlist(self, user_id: int) -> list[dict[str, Any]]:
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT code, name, sector, color, remark, created_at, updated_at
|
||||
FROM watchlist WHERE user_id = ? ORDER BY updated_at DESC, code
|
||||
""",
|
||||
(int(user_id),),
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def save_watchlist(
|
||||
self, user_id: int, code: str, name: str, sector: str, color: str,
|
||||
remark: str | None = None,
|
||||
) -> None:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
with self.connect() as connection:
|
||||
existing = connection.execute(
|
||||
"SELECT remark FROM watchlist WHERE user_id = ? AND code = ?",
|
||||
(int(user_id), code),
|
||||
).fetchone()
|
||||
saved_remark = (
|
||||
str(existing["remark"] or "") if remark is None and existing else str(remark or "")
|
||||
)
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO watchlist
|
||||
(user_id, code, name, sector, color, remark, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, code) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
sector = excluded.sector,
|
||||
color = excluded.color,
|
||||
remark = excluded.remark,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(int(user_id), code, name, sector, color, saved_remark, now, now),
|
||||
)
|
||||
|
||||
def watchlist_price_history(
|
||||
self, codes: list[str], end_date: str, limit_per_code: int = 6
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
result: dict[str, list[dict[str, Any]]] = {}
|
||||
if not codes:
|
||||
return result
|
||||
with self.connect() as connection:
|
||||
for code in codes:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT trade_date, ts_code, close, pct_chg
|
||||
FROM daily_bars
|
||||
WHERE substr(ts_code, 1, 6) = ? AND trade_date <= ?
|
||||
ORDER BY trade_date DESC LIMIT ?
|
||||
""",
|
||||
(str(code), end_date, int(limit_per_code)),
|
||||
).fetchall()
|
||||
result[str(code)] = [dict(row) for row in reversed(rows)]
|
||||
return result
|
||||
|
||||
def delete_watchlist(self, user_id: int, code: str) -> bool:
|
||||
with self.connect() as connection:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM watchlist WHERE user_id = ? AND code = ?",
|
||||
(int(user_id), code),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
def list_notes(
|
||||
self,
|
||||
user_id: int,
|
||||
code: str = "",
|
||||
trade_date: str = "",
|
||||
scope: str = "all",
|
||||
) -> list[dict[str, Any]]:
|
||||
clauses: list[str] = ["user_id = ?"]
|
||||
parameters: list[Any] = [int(user_id)]
|
||||
if scope == "daily":
|
||||
clauses.append("code = ''")
|
||||
elif scope == "stock":
|
||||
clauses.append("code <> ''")
|
||||
if code:
|
||||
clauses.append("code = ?")
|
||||
parameters.append(code)
|
||||
if trade_date:
|
||||
clauses.append("trade_date = ?")
|
||||
parameters.append(trade_date)
|
||||
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT id, code, stock_name, trade_date, summary, content, plan, created_at, updated_at
|
||||
FROM review_notes {where}
|
||||
ORDER BY trade_date DESC, updated_at DESC, id DESC LIMIT 200
|
||||
""",
|
||||
parameters,
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def save_note(
|
||||
self,
|
||||
user_id: int,
|
||||
code: str,
|
||||
stock_name: str,
|
||||
trade_date: str,
|
||||
content: str,
|
||||
plan: str,
|
||||
note_id: int | None = None,
|
||||
summary: str = "",
|
||||
) -> int:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
with self.connect() as connection:
|
||||
if note_id:
|
||||
cursor = connection.execute(
|
||||
"""
|
||||
UPDATE review_notes
|
||||
SET code = ?, stock_name = ?, trade_date = ?, summary = ?, content = ?, plan = ?, updated_at = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
""",
|
||||
(code, stock_name, trade_date, summary, content, plan, now, note_id, int(user_id)),
|
||||
)
|
||||
if cursor.rowcount == 0:
|
||||
raise ValueError("复盘笔记不存在。")
|
||||
return note_id
|
||||
cursor = connection.execute(
|
||||
"""
|
||||
INSERT INTO review_notes
|
||||
(user_id, code, stock_name, trade_date, summary, content, plan, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(int(user_id), code, stock_name, trade_date, summary, content, plan, now, now),
|
||||
)
|
||||
return int(cursor.lastrowid)
|
||||
|
||||
def delete_note(self, user_id: int, note_id: int) -> bool:
|
||||
with self.connect() as connection:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM review_notes WHERE id = ? AND user_id = ?",
|
||||
(note_id, int(user_id)),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
|
||||
def list_sector_phase_overrides(self) -> dict[str, str]:
|
||||
with self.connect() as connection:
|
||||
@@ -832,10 +695,6 @@ class ReviewDatabase(
|
||||
(name,),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
|
||||
|
||||
def list_wencai_saved_queries(
|
||||
self, user_id: int, limit: int = 30
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -885,241 +744,3 @@ class ReviewDatabase(
|
||||
(int(query_id), int(user_id)),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def save_alert(
|
||||
self,
|
||||
user_id: int,
|
||||
kind: str,
|
||||
title: str,
|
||||
content: str,
|
||||
available_date: str,
|
||||
code: str,
|
||||
dedupe_key: str,
|
||||
) -> int:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
with self.connect() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO alerts
|
||||
(user_id, kind, title, content, available_date, code, dedupe_key,
|
||||
is_read, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
||||
ON CONFLICT(user_id, dedupe_key) DO UPDATE SET
|
||||
title=excluded.title, content=excluded.content,
|
||||
available_date=excluded.available_date, updated_at=excluded.updated_at
|
||||
""",
|
||||
(
|
||||
int(user_id), kind, title, content, available_date, code,
|
||||
dedupe_key, now, now,
|
||||
),
|
||||
)
|
||||
row = connection.execute(
|
||||
"SELECT id FROM alerts WHERE user_id = ? AND dedupe_key = ?",
|
||||
(int(user_id), dedupe_key),
|
||||
).fetchone()
|
||||
return int(row["id"])
|
||||
|
||||
def list_alerts(
|
||||
self, user_id: int, as_of: str, unread_only: bool = False, limit: int = 100
|
||||
) -> list[dict[str, Any]]:
|
||||
with self.connect() as connection:
|
||||
if unread_only:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT id, kind, title, content, available_date, code, is_read,
|
||||
created_at, updated_at, read_at
|
||||
FROM alerts
|
||||
WHERE user_id = ? AND available_date <= ? AND is_read = 0
|
||||
ORDER BY available_date DESC, id DESC LIMIT ?
|
||||
""",
|
||||
(int(user_id), as_of, max(1, min(300, int(limit)))),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT id, kind, title, content, available_date, code, is_read,
|
||||
created_at, updated_at, read_at
|
||||
FROM alerts WHERE user_id = ?
|
||||
ORDER BY CASE WHEN available_date > ? THEN 0 ELSE 1 END,
|
||||
is_read, available_date, id DESC LIMIT ?
|
||||
""",
|
||||
(int(user_id), as_of, max(1, min(300, int(limit)))),
|
||||
).fetchall()
|
||||
return [{**dict(row), "is_read": bool(row["is_read"])} for row in rows]
|
||||
|
||||
def count_unread_alerts(self, user_id: int, as_of: str) -> int:
|
||||
with self.connect() as connection:
|
||||
row = connection.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS total FROM alerts
|
||||
WHERE user_id = ? AND available_date <= ? AND is_read = 0
|
||||
""",
|
||||
(int(user_id), as_of),
|
||||
).fetchone()
|
||||
return int(row["total"] if row else 0)
|
||||
|
||||
def mark_alert_read(self, user_id: int, alert_id: int) -> bool:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
with self.connect() as connection:
|
||||
cursor = connection.execute(
|
||||
"""
|
||||
UPDATE alerts SET is_read = 1, read_at = ?, updated_at = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
""",
|
||||
(now, now, int(alert_id), int(user_id)),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def mark_all_alerts_read(self, user_id: int, as_of: str) -> int:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
with self.connect() as connection:
|
||||
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, int(user_id), as_of),
|
||||
)
|
||||
return int(cursor.rowcount)
|
||||
|
||||
def delete_alert(self, user_id: int, alert_id: int) -> bool:
|
||||
with self.connect() as connection:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM alerts WHERE id = ? AND user_id = ?",
|
||||
(int(alert_id), int(user_id)),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def save_trade_entry(
|
||||
self,
|
||||
user_id: int,
|
||||
trade_date: str,
|
||||
code: str,
|
||||
name: str,
|
||||
action: str,
|
||||
price: float,
|
||||
quantity: int,
|
||||
position_pct: float,
|
||||
pnl_amount: float | None,
|
||||
pnl_pct: float | None,
|
||||
thesis: str,
|
||||
execution: str,
|
||||
emotion: str,
|
||||
tags: list[str],
|
||||
trade_id: int | None = None,
|
||||
) -> int:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
tags_json = json.dumps(tags, ensure_ascii=False, separators=(",", ":"))
|
||||
with self.connect() as connection:
|
||||
if trade_id:
|
||||
cursor = connection.execute(
|
||||
"""
|
||||
UPDATE trade_entries SET
|
||||
trade_date=?, code=?, name=?, action=?, price=?, quantity=?,
|
||||
position_pct=?, pnl_amount=?, pnl_pct=?, thesis=?, execution=?,
|
||||
emotion=?, tags=?, updated_at=?
|
||||
WHERE id=? AND user_id=?
|
||||
""",
|
||||
(
|
||||
trade_date, code, name, action, price, quantity, position_pct,
|
||||
pnl_amount, pnl_pct, thesis, execution, emotion, tags_json, now,
|
||||
int(trade_id), int(user_id),
|
||||
),
|
||||
)
|
||||
if cursor.rowcount == 0:
|
||||
raise ValueError("交易记录不存在或无权修改。")
|
||||
return int(trade_id)
|
||||
cursor = connection.execute(
|
||||
"""
|
||||
INSERT INTO trade_entries
|
||||
(user_id, trade_date, code, name, action, price, quantity,
|
||||
position_pct, pnl_amount, pnl_pct, thesis, execution, emotion,
|
||||
tags, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
int(user_id), trade_date, code, name, action, price, quantity,
|
||||
position_pct, pnl_amount, pnl_pct, thesis, execution, emotion,
|
||||
tags_json, now, now,
|
||||
),
|
||||
)
|
||||
return int(cursor.lastrowid)
|
||||
|
||||
def list_trade_entries(
|
||||
self, user_id: int, start_date: str = "", end_date: str = "", code: str = "",
|
||||
limit: int = 300,
|
||||
) -> list[dict[str, Any]]:
|
||||
clauses = ["user_id = ?"]
|
||||
parameters: list[Any] = [int(user_id)]
|
||||
if start_date:
|
||||
clauses.append("trade_date >= ?")
|
||||
parameters.append(start_date)
|
||||
if end_date:
|
||||
clauses.append("trade_date <= ?")
|
||||
parameters.append(end_date)
|
||||
if code:
|
||||
clauses.append("code = ?")
|
||||
parameters.append(code)
|
||||
parameters.append(max(1, min(1000, int(limit))))
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT * FROM trade_entries WHERE {' AND '.join(clauses)}
|
||||
ORDER BY trade_date DESC, id DESC LIMIT ?
|
||||
""",
|
||||
parameters,
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def delete_trade_entry(self, user_id: int, trade_id: int) -> bool:
|
||||
with self.connect() as connection:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM trade_entries WHERE id = ? AND user_id = ?",
|
||||
(int(trade_id), int(user_id)),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def save_assistant_exchange(
|
||||
self, user_id: int, question: str, answer: str, context_date: str
|
||||
) -> None:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
with self.connect() as connection:
|
||||
connection.executemany(
|
||||
"""
|
||||
INSERT INTO assistant_messages
|
||||
(user_id, role, content, context_date, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
(int(user_id), "user", question, context_date, now),
|
||||
(int(user_id), "assistant", answer, context_date, now),
|
||||
],
|
||||
)
|
||||
connection.execute(
|
||||
"""
|
||||
DELETE FROM assistant_messages WHERE user_id = ? AND id NOT IN (
|
||||
SELECT id FROM assistant_messages
|
||||
WHERE user_id = ? ORDER BY id DESC LIMIT 200
|
||||
)
|
||||
""",
|
||||
(int(user_id), int(user_id)),
|
||||
)
|
||||
|
||||
def list_assistant_messages(self, user_id: int, limit: int = 100) -> list[dict[str, Any]]:
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT role, content, context_date, created_at FROM assistant_messages
|
||||
WHERE user_id = ? ORDER BY id DESC LIMIT ?
|
||||
""",
|
||||
(int(user_id), max(1, min(200, int(limit)))),
|
||||
).fetchall()
|
||||
return [dict(row) for row in reversed(rows)]
|
||||
|
||||
def delete_assistant_messages(self, user_id: int) -> int:
|
||||
with self.connect() as connection:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM assistant_messages WHERE user_id = ?", (int(user_id),)
|
||||
)
|
||||
return int(cursor.rowcount)
|
||||
|
||||
Reference in New Issue
Block a user