1126 lines
47 KiB
Python
1126 lines
47 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from backend.database import MIGRATIONS, MigrationRunner, SQLiteConnectionFactory
|
|
from backend.features.accounts.repository import AccountRepositoryMixin
|
|
from backend.features.auction.repository import AuctionRepositoryMixin
|
|
from backend.features.dragon_tiger.repository import DragonTigerRepositoryMixin
|
|
from backend.features.heaven.repository import HeavenRepositoryMixin
|
|
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.screener.repository import ScreenerRepositoryMixin
|
|
from backend.features.system.repository import SystemSettingsRepositoryMixin
|
|
from backend.llm.repository import LLMAuditRepositoryMixin
|
|
|
|
|
|
class ReviewDatabase(
|
|
AccountRepositoryMixin,
|
|
AuctionRepositoryMixin,
|
|
DragonTigerRepositoryMixin,
|
|
HeavenRepositoryMixin,
|
|
MarketRepositoryMixin,
|
|
MentorRepositoryMixin,
|
|
PoolRepositoryMixin,
|
|
PopularityRepositoryMixin,
|
|
ScreenerRepositoryMixin,
|
|
SystemSettingsRepositoryMixin,
|
|
LLMAuditRepositoryMixin,
|
|
):
|
|
def __init__(self, path: Path) -> None:
|
|
self.path = path
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self.connection_factory = SQLiteConnectionFactory(self.path)
|
|
self._initialize()
|
|
|
|
def connect(self) -> sqlite3.Connection:
|
|
return self.connection_factory.connect()
|
|
|
|
def _initialize(self) -> None:
|
|
with self.connect() as connection:
|
|
connection.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
password_salt TEXT NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
token_hash TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL,
|
|
csrf_token TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
last_seen_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_user_sessions_user
|
|
ON user_sessions(user_id, expires_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_credentials (
|
|
user_id INTEGER PRIMARY KEY,
|
|
encrypted_payload TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_birth_profiles (
|
|
user_id INTEGER PRIMARY KEY,
|
|
encrypted_payload TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS system_settings (
|
|
setting_key TEXT PRIMARY KEY,
|
|
encrypted_payload TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS llm_usage (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
feature TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
model TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL,
|
|
latency_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_llm_usage_user_time
|
|
ON llm_usage(user_id, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS dashboard_snapshots (
|
|
trade_date TEXT PRIMARY KEY,
|
|
source TEXT NOT NULL,
|
|
payload TEXT NOT NULL,
|
|
record_count INTEGER NOT NULL DEFAULT 0,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sync_runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
trade_date TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
started_at TEXT NOT NULL,
|
|
finished_at TEXT,
|
|
record_count INTEGER NOT NULL DEFAULT 0,
|
|
message TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sync_runs_trade_date
|
|
ON sync_runs(trade_date, id DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS data_snapshots (
|
|
kind TEXT NOT NULL,
|
|
cache_key TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
payload TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (kind, cache_key)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS watchlist (
|
|
user_id INTEGER NOT NULL,
|
|
code TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
sector TEXT NOT NULL DEFAULT '',
|
|
color TEXT NOT NULL DEFAULT 'red',
|
|
remark TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (user_id, code),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS review_notes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
code TEXT NOT NULL DEFAULT '',
|
|
stock_name TEXT NOT NULL DEFAULT '',
|
|
trade_date TEXT NOT NULL,
|
|
summary TEXT NOT NULL DEFAULT '',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
plan TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_review_notes_code_date
|
|
ON review_notes(code, trade_date DESC, id DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS reason_overrides (
|
|
trade_date TEXT NOT NULL,
|
|
code TEXT NOT NULL,
|
|
reason TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (trade_date, code)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS seat_aliases (
|
|
seat_name TEXT PRIMARY KEY,
|
|
alias TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sector_phase_overrides (
|
|
name TEXT PRIMARY KEY,
|
|
element TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS stock_master (
|
|
ts_code TEXT PRIMARY KEY,
|
|
code TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
industry TEXT NOT NULL DEFAULT '',
|
|
market TEXT NOT NULL DEFAULT '',
|
|
list_date TEXT NOT NULL DEFAULT '',
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_stock_master_code ON stock_master(code);
|
|
|
|
CREATE TABLE IF NOT EXISTS daily_bars (
|
|
trade_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
open REAL NOT NULL DEFAULT 0,
|
|
high REAL NOT NULL DEFAULT 0,
|
|
low REAL NOT NULL DEFAULT 0,
|
|
close REAL NOT NULL DEFAULT 0,
|
|
pct_chg REAL NOT NULL DEFAULT 0,
|
|
vol REAL NOT NULL DEFAULT 0,
|
|
amount REAL NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (trade_date, ts_code)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_daily_bars_code_date
|
|
ON daily_bars(ts_code, trade_date DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS benchmark_bars (
|
|
trade_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
close REAL NOT NULL DEFAULT 0,
|
|
pct_chg REAL NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (trade_date, ts_code)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_benchmark_bars_code_date
|
|
ON benchmark_bars(ts_code, trade_date DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS daily_indicators (
|
|
trade_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
turnover_rate REAL NOT NULL DEFAULT 0,
|
|
volume_ratio REAL NOT NULL DEFAULT 0,
|
|
total_mv REAL NOT NULL DEFAULT 0,
|
|
circ_mv REAL NOT NULL DEFAULT 0,
|
|
pe_ttm REAL,
|
|
pb REAL,
|
|
ps_ttm REAL,
|
|
dv_ttm REAL,
|
|
PRIMARY KEY (trade_date, ts_code)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS fundamental_indicators (
|
|
end_date TEXT NOT NULL,
|
|
ann_date TEXT NOT NULL DEFAULT '',
|
|
ts_code TEXT NOT NULL,
|
|
roe REAL,
|
|
roa REAL,
|
|
roic REAL,
|
|
grossprofit_margin REAL,
|
|
netprofit_yoy REAL,
|
|
or_yoy REAL,
|
|
ocf_to_opincome REAL,
|
|
PRIMARY KEY (end_date, ts_code)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_fundamental_indicators_code_date
|
|
ON fundamental_indicators(ts_code, ann_date DESC, end_date DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS moneyflow_daily (
|
|
trade_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
net_mf_amount REAL NOT NULL DEFAULT 0,
|
|
large_net_amount REAL NOT NULL DEFAULT 0,
|
|
medium_net_amount REAL NOT NULL DEFAULT 0,
|
|
small_net_amount REAL NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (trade_date, ts_code)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS auction_factors (
|
|
trade_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
price REAL NOT NULL DEFAULT 0,
|
|
pre_close REAL NOT NULL DEFAULT 0,
|
|
change REAL NOT NULL DEFAULT 0,
|
|
vol REAL NOT NULL DEFAULT 0,
|
|
amount REAL NOT NULL DEFAULT 0,
|
|
turnover_rate REAL NOT NULL DEFAULT 0,
|
|
volume_ratio REAL NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (trade_date, ts_code)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_auction_factors_code_date
|
|
ON auction_factors(ts_code, trade_date DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS earnings_events (
|
|
end_date TEXT NOT NULL,
|
|
ann_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
forecast_profit REAL,
|
|
actual_profit REAL,
|
|
surprise_pct REAL,
|
|
revenue_yoy REAL,
|
|
netprofit_yoy REAL,
|
|
source TEXT NOT NULL DEFAULT '',
|
|
PRIMARY KEY (end_date, ann_date, ts_code)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_earnings_events_code_announcement
|
|
ON earnings_events(ts_code, ann_date DESC, end_date DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS popularity_factors (
|
|
trade_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
ths_rank INTEGER,
|
|
dc_rank INTEGER,
|
|
combined_score REAL NOT NULL DEFAULT 0,
|
|
rank_change INTEGER,
|
|
dual_source INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (trade_date, ts_code)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_popularity_factors_code_date
|
|
ON popularity_factors(ts_code, trade_date DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS lhb_institution_daily (
|
|
trade_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
net_buy_amount REAL NOT NULL DEFAULT 0,
|
|
buy_amount REAL NOT NULL DEFAULT 0,
|
|
sell_amount REAL NOT NULL DEFAULT 0,
|
|
seat_count INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (trade_date, ts_code)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lhb_institution_code_date
|
|
ON lhb_institution_daily(ts_code, trade_date DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS screener_strategies (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER,
|
|
name TEXT NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
regimes TEXT NOT NULL,
|
|
formula TEXT NOT NULL,
|
|
builtin INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS screener_runs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER,
|
|
trade_date TEXT NOT NULL,
|
|
regime TEXT NOT NULL,
|
|
mode TEXT NOT NULL DEFAULT 'smart',
|
|
strategy_name TEXT NOT NULL,
|
|
formula TEXT NOT NULL,
|
|
result TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS mentor_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
mentor_id TEXT NOT NULL,
|
|
trade_date TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
meta TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_mentor_messages_conversation
|
|
ON mentor_messages(user_id, mentor_id, trade_date, id DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS mentor_preferences (
|
|
user_id INTEGER NOT NULL,
|
|
mentor_id TEXT NOT NULL,
|
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (user_id, mentor_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_mentor_preferences_user_order
|
|
ON mentor_preferences(user_id, pinned DESC, sort_order, mentor_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS wencai_saved_queries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
title TEXT NOT NULL,
|
|
query TEXT NOT NULL,
|
|
search_type TEXT NOT NULL DEFAULT 'stock',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE(user_id, query, search_type),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wencai_saved_queries_user
|
|
ON wencai_saved_queries(user_id, updated_at DESC, id DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS strategy_tracks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
run_id INTEGER NOT NULL,
|
|
selection_date TEXT NOT NULL,
|
|
strategy_name TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
code TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
sector TEXT NOT NULL DEFAULT '',
|
|
entry_price REAL NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE(user_id, run_id, ts_code),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (run_id) REFERENCES screener_runs(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_strategy_tracks_user_run
|
|
ON strategy_tracks(user_id, run_id DESC, id);
|
|
|
|
CREATE TABLE IF NOT EXISTS alerts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
content TEXT NOT NULL DEFAULT '',
|
|
available_date TEXT NOT NULL,
|
|
code TEXT NOT NULL DEFAULT '',
|
|
dedupe_key TEXT NOT NULL,
|
|
is_read INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
read_at TEXT,
|
|
UNIQUE(user_id, dedupe_key),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_alerts_user_due
|
|
ON alerts(user_id, available_date, is_read, id DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS trade_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
trade_date TEXT NOT NULL,
|
|
code TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
price REAL NOT NULL,
|
|
quantity INTEGER NOT NULL DEFAULT 0,
|
|
position_pct REAL NOT NULL DEFAULT 0,
|
|
pnl_amount REAL,
|
|
pnl_pct REAL,
|
|
thesis TEXT NOT NULL DEFAULT '',
|
|
execution TEXT NOT NULL DEFAULT '',
|
|
emotion TEXT NOT NULL DEFAULT 'calm',
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_trade_entries_user_date
|
|
ON trade_entries(user_id, trade_date DESC, id DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS assistant_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
role TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
context_date TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_assistant_messages_user
|
|
ON assistant_messages(user_id, id DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS heaven_readings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
mode TEXT NOT NULL,
|
|
context_date TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
subject_detail TEXT NOT NULL DEFAULT '',
|
|
answer TEXT NOT NULL,
|
|
context_snapshot TEXT NOT NULL DEFAULT '{}',
|
|
dedupe_key TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(user_id, dedupe_key),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_heaven_readings_user_mode
|
|
ON heaven_readings(user_id, mode, context_date DESC, id DESC);
|
|
"""
|
|
)
|
|
user_columns = {
|
|
str(row["name"]) for row in connection.execute("PRAGMA table_info(users)")
|
|
}
|
|
migrations = {
|
|
"role": "ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'",
|
|
"llm_mode": "ALTER TABLE users ADD COLUMN llm_mode TEXT NOT NULL DEFAULT 'auto'",
|
|
"membership_status": "ALTER TABLE users ADD COLUMN membership_status TEXT NOT NULL DEFAULT 'inactive'",
|
|
"membership_plan": "ALTER TABLE users ADD COLUMN membership_plan TEXT NOT NULL DEFAULT ''",
|
|
"membership_starts_at": "ALTER TABLE users ADD COLUMN membership_starts_at TEXT",
|
|
"membership_expires_at": "ALTER TABLE users ADD COLUMN membership_expires_at TEXT",
|
|
}
|
|
for column, statement in migrations.items():
|
|
if column not in user_columns:
|
|
connection.execute(statement)
|
|
indicator_columns = {
|
|
str(row["name"])
|
|
for row in connection.execute("PRAGMA table_info(daily_indicators)")
|
|
}
|
|
indicator_migrations = {
|
|
"pe_ttm": "ALTER TABLE daily_indicators ADD COLUMN pe_ttm REAL",
|
|
"pb": "ALTER TABLE daily_indicators ADD COLUMN pb REAL",
|
|
"ps_ttm": "ALTER TABLE daily_indicators ADD COLUMN ps_ttm REAL",
|
|
"dv_ttm": "ALTER TABLE daily_indicators ADD COLUMN dv_ttm REAL",
|
|
}
|
|
for column, statement in indicator_migrations.items():
|
|
if column not in indicator_columns:
|
|
connection.execute(statement)
|
|
connection.execute(
|
|
"""
|
|
UPDATE users SET role = 'admin'
|
|
WHERE id = (SELECT MIN(id) FROM users)
|
|
AND NOT EXISTS (SELECT 1 FROM users WHERE role = 'admin')
|
|
"""
|
|
)
|
|
watchlist_columns = {
|
|
str(row["name"]) for row in connection.execute("PRAGMA table_info(watchlist)")
|
|
}
|
|
if "user_id" not in watchlist_columns:
|
|
connection.execute("ALTER TABLE watchlist RENAME TO watchlist_legacy")
|
|
connection.execute(
|
|
"""
|
|
CREATE TABLE watchlist (
|
|
user_id INTEGER NOT NULL,
|
|
code TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
sector TEXT NOT NULL DEFAULT '',
|
|
color TEXT NOT NULL DEFAULT 'red',
|
|
remark TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (user_id, code),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)
|
|
"""
|
|
)
|
|
first_user = connection.execute("SELECT MIN(id) AS id FROM users").fetchone()
|
|
if first_user and first_user["id"]:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO watchlist
|
|
(user_id, code, name, sector, color, created_at, updated_at)
|
|
SELECT ?, code, name, sector, color, created_at, updated_at
|
|
FROM watchlist_legacy
|
|
""",
|
|
(int(first_user["id"]),),
|
|
)
|
|
connection.execute("DROP TABLE watchlist_legacy")
|
|
watchlist_columns.add("remark")
|
|
if "remark" not in watchlist_columns:
|
|
connection.execute(
|
|
"ALTER TABLE watchlist ADD COLUMN remark TEXT NOT NULL DEFAULT ''"
|
|
)
|
|
note_columns = {
|
|
str(row["name"]) for row in connection.execute("PRAGMA table_info(review_notes)")
|
|
}
|
|
if "user_id" not in note_columns:
|
|
connection.execute("ALTER TABLE review_notes ADD COLUMN user_id INTEGER")
|
|
if "summary" not in note_columns:
|
|
connection.execute(
|
|
"ALTER TABLE review_notes ADD COLUMN summary TEXT NOT NULL DEFAULT ''"
|
|
)
|
|
first_user = connection.execute("SELECT MIN(id) AS id FROM users").fetchone()
|
|
if first_user and first_user["id"]:
|
|
connection.execute(
|
|
"UPDATE review_notes SET user_id = ? WHERE user_id IS NULL",
|
|
(int(first_user["id"]),),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_review_notes_user_date
|
|
ON review_notes(user_id, trade_date DESC, id DESC)
|
|
"""
|
|
)
|
|
strategy_columns = {
|
|
str(row["name"]) for row in connection.execute("PRAGMA table_info(screener_strategies)")
|
|
}
|
|
if "user_id" not in strategy_columns:
|
|
connection.execute("ALTER TABLE screener_strategies ADD COLUMN user_id INTEGER")
|
|
run_columns = {
|
|
str(row["name"]) for row in connection.execute("PRAGMA table_info(screener_runs)")
|
|
}
|
|
legacy_run_ownership = "user_id" not in run_columns
|
|
if "user_id" not in run_columns:
|
|
connection.execute("ALTER TABLE screener_runs ADD COLUMN user_id INTEGER")
|
|
if "mode" not in run_columns:
|
|
connection.execute(
|
|
"ALTER TABLE screener_runs ADD COLUMN mode TEXT NOT NULL DEFAULT 'smart'"
|
|
)
|
|
legacy_runs = connection.execute(
|
|
"SELECT id, strategy_name, formula FROM screener_runs"
|
|
).fetchall()
|
|
for run in legacy_runs:
|
|
try:
|
|
formula = json.loads(run["formula"])
|
|
except (TypeError, json.JSONDecodeError):
|
|
formula = {}
|
|
meta = formula.get("meta") if isinstance(formula, dict) else {}
|
|
library = str((meta or {}).get("library") or "")
|
|
category = str((meta or {}).get("category") or "")
|
|
if library == "curated":
|
|
mode = "curated"
|
|
elif library == "quant" or (
|
|
library == "custom" and category == "量化公式"
|
|
) or str(run["strategy_name"] or "") == "自定义量化公式":
|
|
mode = "quant"
|
|
else:
|
|
mode = "smart"
|
|
connection.execute(
|
|
"UPDATE screener_runs SET mode = ? WHERE id = ?",
|
|
(mode, int(run["id"])),
|
|
)
|
|
connection.execute(
|
|
"UPDATE screener_runs SET user_id = NULL WHERE user_id = 0"
|
|
)
|
|
if first_user and first_user["id"]:
|
|
first_user_id = int(first_user["id"])
|
|
connection.execute(
|
|
"UPDATE screener_strategies SET user_id = ? WHERE builtin = 0 AND user_id IS NULL",
|
|
(first_user_id,),
|
|
)
|
|
if legacy_run_ownership:
|
|
connection.execute(
|
|
"UPDATE screener_runs SET user_id = ? WHERE user_id IS NULL",
|
|
(first_user_id,),
|
|
)
|
|
else:
|
|
connection.execute(
|
|
"""
|
|
UPDATE screener_runs SET user_id = ?
|
|
WHERE user_id IS NULL AND mode = 'quant'
|
|
""",
|
|
(first_user_id,),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_screener_strategies_user
|
|
ON screener_strategies(user_id, builtin, updated_at DESC)
|
|
"""
|
|
)
|
|
connection.execute(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_screener_runs_user_date
|
|
ON screener_runs(user_id, trade_date DESC, id DESC)
|
|
"""
|
|
)
|
|
connection.execute(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_screener_runs_user_mode_date
|
|
ON screener_runs(user_id, mode, trade_date DESC, id DESC)
|
|
"""
|
|
)
|
|
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:
|
|
rows = connection.execute(
|
|
"SELECT name, element FROM sector_phase_overrides ORDER BY updated_at DESC, name"
|
|
).fetchall()
|
|
return {row["name"]: row["element"] for row in rows}
|
|
|
|
def save_sector_phase_override(self, name: str, element: str) -> None:
|
|
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
with self.connect() as connection:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO sector_phase_overrides (name, element, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
element = excluded.element,
|
|
updated_at = excluded.updated_at
|
|
""",
|
|
(name, element, now),
|
|
)
|
|
|
|
def delete_sector_phase_override(self, name: str) -> bool:
|
|
with self.connect() as connection:
|
|
cursor = connection.execute(
|
|
"DELETE FROM sector_phase_overrides WHERE name = ?",
|
|
(name,),
|
|
)
|
|
return cursor.rowcount > 0
|
|
|
|
|
|
|
|
|
|
def list_wencai_saved_queries(
|
|
self, user_id: int, limit: int = 30
|
|
) -> list[dict[str, Any]]:
|
|
with self.connect() as connection:
|
|
rows = connection.execute(
|
|
"""
|
|
SELECT id, title, query, search_type, created_at, updated_at
|
|
FROM wencai_saved_queries
|
|
WHERE user_id = ?
|
|
ORDER BY updated_at DESC, id DESC LIMIT ?
|
|
""",
|
|
(int(user_id), max(1, min(100, int(limit)))),
|
|
).fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
def save_wencai_query(
|
|
self, user_id: int, title: str, query: str, search_type: str = "stock"
|
|
) -> int:
|
|
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
with self.connect() as connection:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO wencai_saved_queries
|
|
(user_id, title, query, search_type, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(user_id, query, search_type) DO UPDATE SET
|
|
title = excluded.title,
|
|
updated_at = excluded.updated_at
|
|
""",
|
|
(int(user_id), title, query, search_type, now, now),
|
|
)
|
|
row = connection.execute(
|
|
"""
|
|
SELECT id FROM wencai_saved_queries
|
|
WHERE user_id = ? AND query = ? AND search_type = ?
|
|
""",
|
|
(int(user_id), query, search_type),
|
|
).fetchone()
|
|
if not row:
|
|
raise ValueError("问财条件保存失败。")
|
|
return int(row["id"])
|
|
|
|
def delete_wencai_saved_query(self, user_id: int, query_id: int) -> bool:
|
|
with self.connect() as connection:
|
|
cursor = connection.execute(
|
|
"DELETE FROM wencai_saved_queries WHERE id = ? AND user_id = ?",
|
|
(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)
|