172 lines
12 KiB
Python
172 lines
12 KiB
Python
"""SQLAlchemy 异步数据库配置"""
|
||
|
||
from contextlib import asynccontextmanager
|
||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||
from sqlalchemy import event
|
||
from app.config import settings
|
||
|
||
# SQLite 异步需要 aiosqlite
|
||
db_url = settings.database_url.replace("sqlite:///", "sqlite+aiosqlite:///")
|
||
engine = create_async_engine(db_url, echo=False)
|
||
|
||
# 启用 WAL 模式:允许读写并发,写操作不阻塞读操作
|
||
@event.listens_for(engine.sync_engine, "connect")
|
||
def _set_sqlite_pragma(dbapi_connection, connection_record):
|
||
cursor = dbapi_connection.cursor()
|
||
cursor.execute("PRAGMA journal_mode=WAL")
|
||
cursor.execute("PRAGMA busy_timeout=5000") # 写锁最多等待 5 秒
|
||
cursor.close()
|
||
|
||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def get_db():
|
||
session = async_session()
|
||
try:
|
||
yield session
|
||
finally:
|
||
await session.close()
|
||
|
||
|
||
async def init_db():
|
||
"""创建所有表,并补充新增列"""
|
||
from app.db.tables import metadata
|
||
async with engine.begin() as conn:
|
||
await conn.run_sync(metadata.create_all)
|
||
# 补充新增列(SQLite ALTER TABLE ADD COLUMN,已存在会忽略)
|
||
for col_sql in [
|
||
"ALTER TABLE recommendations ADD COLUMN supply_demand_score REAL DEFAULT 0",
|
||
"ALTER TABLE recommendations ADD COLUMN price_action_score REAL DEFAULT 0",
|
||
"ALTER TABLE recommendations ADD COLUMN position_score REAL",
|
||
"ALTER TABLE recommendations ADD COLUMN valuation_score REAL",
|
||
"ALTER TABLE recommendations ADD COLUMN risk_note TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendations ADD COLUMN action_plan TEXT DEFAULT '观察'",
|
||
"ALTER TABLE recommendations ADD COLUMN trigger_condition TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendations ADD COLUMN invalidation_condition TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendations ADD COLUMN suggested_position_pct REAL DEFAULT 0",
|
||
"ALTER TABLE recommendations ADD COLUMN review_after_days INTEGER DEFAULT 3",
|
||
"ALTER TABLE recommendations ADD COLUMN lifecycle_status TEXT DEFAULT 'candidate'",
|
||
"ALTER TABLE recommendations ADD COLUMN data_freshness TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendations ADD COLUMN llm_analysis TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendations ADD COLUMN strategy TEXT DEFAULT 'momentum'",
|
||
"ALTER TABLE recommendations ADD COLUMN llm_score REAL",
|
||
"ALTER TABLE market_temperature ADD COLUMN max_streak INTEGER",
|
||
"ALTER TABLE market_temperature ADD COLUMN broken_rate REAL",
|
||
"ALTER TABLE recommendations ADD COLUMN entry_signal_type TEXT DEFAULT 'none'",
|
||
"ALTER TABLE recommendations ADD COLUMN entry_timing TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendations ADD COLUMN recall_tags TEXT DEFAULT '[]'",
|
||
"ALTER TABLE recommendations ADD COLUMN prefilter_decision TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendations ADD COLUMN prefilter_reason TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendations ADD COLUMN focus_points TEXT DEFAULT '[]'",
|
||
"ALTER TABLE recommendations ADD COLUMN decision_trace TEXT DEFAULT '{}'",
|
||
"ALTER TABLE sector_heat ADD COLUMN stage TEXT",
|
||
"ALTER TABLE sector_heat ADD COLUMN board_type TEXT DEFAULT 'theme'",
|
||
"ALTER TABLE sector_heat ADD COLUMN theme_id TEXT DEFAULT ''",
|
||
"ALTER TABLE sector_heat ADD COLUMN theme_name TEXT DEFAULT ''",
|
||
"ALTER TABLE sector_heat ADD COLUMN theme_aliases TEXT DEFAULT '[]'",
|
||
"ALTER TABLE sector_heat ADD COLUMN days_continuous INTEGER",
|
||
"ALTER TABLE sector_heat ADD COLUMN member_count INTEGER",
|
||
"ALTER TABLE sector_heat ADD COLUMN leading_stocks TEXT",
|
||
"ALTER TABLE sector_heat ADD COLUMN pct_trend TEXT",
|
||
"ALTER TABLE sector_heat ADD COLUMN turnover_avg REAL",
|
||
"ALTER TABLE sector_heat ADD COLUMN main_force_ratio REAL",
|
||
"ALTER TABLE recommendation_tracking ADD COLUMN max_price REAL",
|
||
"ALTER TABLE recommendation_tracking ADD COLUMN min_price REAL",
|
||
"ALTER TABLE recommendation_tracking ADD COLUMN max_return_pct REAL",
|
||
"ALTER TABLE recommendation_tracking ADD COLUMN max_drawdown_pct REAL",
|
||
"ALTER TABLE recommendation_tracking ADD COLUMN days_since_recommendation INTEGER DEFAULT 0",
|
||
"ALTER TABLE recommendation_tracking ADD COLUMN close_reason TEXT DEFAULT ''",
|
||
"ALTER TABLE recommendation_tracking ADD COLUMN review_note TEXT DEFAULT ''",
|
||
"ALTER TABLE users ADD COLUMN email TEXT",
|
||
"ALTER TABLE users ADD COLUMN invite_code_used TEXT DEFAULT ''",
|
||
"ALTER TABLE stock_diagnoses ADD COLUMN diagnosis_mode TEXT DEFAULT 'entry'",
|
||
"ALTER TABLE user_watchlists ADD COLUMN note TEXT DEFAULT ''",
|
||
"ALTER TABLE user_watchlists ADD COLUMN watch_group TEXT DEFAULT 'observe'",
|
||
"ALTER TABLE user_watchlists ADD COLUMN cost_price REAL",
|
||
"ALTER TABLE user_watchlists ADD COLUMN is_active BOOLEAN DEFAULT 1",
|
||
"ALTER TABLE user_watchlists ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP",
|
||
"ALTER TABLE watchlist_analyses ADD COLUMN conclusion TEXT DEFAULT '观察'",
|
||
"ALTER TABLE watchlist_analyses ADD COLUMN advice TEXT DEFAULT ''",
|
||
"ALTER TABLE watchlist_analyses ADD COLUMN trigger_condition TEXT DEFAULT ''",
|
||
"ALTER TABLE watchlist_analyses ADD COLUMN risk_note TEXT DEFAULT ''",
|
||
"ALTER TABLE watchlist_analyses ADD COLUMN summary TEXT DEFAULT ''",
|
||
"ALTER TABLE watchlist_analyses ADD COLUMN full_analysis TEXT DEFAULT ''",
|
||
"ALTER TABLE watchlist_analyses ADD COLUMN score_reference REAL DEFAULT 0",
|
||
"ALTER TABLE watchlist_analyses ADD COLUMN analysis_mode TEXT DEFAULT 'scheduled'",
|
||
"ALTER TABLE strategy_configs ADD COLUMN evidence_json TEXT DEFAULT '{}'",
|
||
"ALTER TABLE strategy_configs ADD COLUMN effective_from DATETIME DEFAULT CURRENT_TIMESTAMP",
|
||
"ALTER TABLE prompt_configs ADD COLUMN evidence_json TEXT DEFAULT '{}'",
|
||
"ALTER TABLE strategy_config_changes ADD COLUMN prompt_key TEXT DEFAULT ''",
|
||
"ALTER TABLE news_items ADD COLUMN summary TEXT DEFAULT ''",
|
||
"ALTER TABLE news_items ADD COLUMN error TEXT DEFAULT ''",
|
||
"ALTER TABLE catalysts ADD COLUMN llm_reason TEXT DEFAULT ''",
|
||
"ALTER TABLE stock_research_notes ADD COLUMN disagreement TEXT DEFAULT ''",
|
||
"ALTER TABLE stock_research_notes ADD COLUMN invalid_condition TEXT DEFAULT ''",
|
||
"ALTER TABLE stock_research_notes ADD COLUMN generated_by TEXT DEFAULT 'rules'",
|
||
"ALTER TABLE stock_research_notes ADD COLUMN stock_role TEXT DEFAULT '待归类'",
|
||
"ALTER TABLE stock_research_notes ADD COLUMN theme TEXT DEFAULT '未归类'",
|
||
"ALTER TABLE stock_research_notes ADD COLUMN chain_node TEXT DEFAULT '未归类'",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN stock_role TEXT DEFAULT '待归类'",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN alpha_type TEXT DEFAULT '观察线索'",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN alpha_score REAL DEFAULT 0",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN beta_dependency TEXT DEFAULT '中'",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN beta_dependency_score REAL DEFAULT 0",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN ambush_score REAL DEFAULT 0",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN expectation_gap_score REAL DEFAULT 0",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN risk_gate TEXT DEFAULT '通过'",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN setup_quality TEXT DEFAULT '仅观察'",
|
||
"ALTER TABLE opportunity_cards ADD COLUMN alpha_reason TEXT DEFAULT ''",
|
||
"ALTER TABLE theme_knowledge ADD COLUMN lifecycle_status TEXT DEFAULT '观察期'",
|
||
"ALTER TABLE theme_knowledge ADD COLUMN stage TEXT DEFAULT 'mid'",
|
||
"ALTER TABLE theme_knowledge ADD COLUMN is_active BOOLEAN DEFAULT 1",
|
||
"ALTER TABLE theme_knowledge ADD COLUMN sort_order INTEGER DEFAULT 0",
|
||
"ALTER TABLE theme_knowledge ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP",
|
||
"ALTER TABLE theme_chain_knowledge ADD COLUMN related_stocks TEXT DEFAULT '[]'",
|
||
"ALTER TABLE theme_chain_knowledge ADD COLUMN leader_stocks TEXT DEFAULT '[]'",
|
||
"ALTER TABLE theme_chain_knowledge ADD COLUMN node_role TEXT DEFAULT ''",
|
||
"ALTER TABLE theme_chain_knowledge ADD COLUMN is_active BOOLEAN DEFAULT 1",
|
||
"ALTER TABLE theme_chain_knowledge ADD COLUMN sort_order INTEGER DEFAULT 0",
|
||
"ALTER TABLE theme_chain_knowledge ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP",
|
||
]:
|
||
try:
|
||
await conn.execute(
|
||
__import__("sqlalchemy").text(col_sql)
|
||
)
|
||
except Exception:
|
||
pass # 列已存在,忽略
|
||
|
||
for index_sql in [
|
||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_news_items_dedup_key ON news_items(dedup_key)",
|
||
"CREATE INDEX IF NOT EXISTS idx_news_items_status_time ON news_items(status, published_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_catalysts_source_url ON catalysts(source, url)",
|
||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_theme_knowledge_name ON theme_knowledge(theme_name)",
|
||
"CREATE INDEX IF NOT EXISTS idx_theme_knowledge_active_order ON theme_knowledge(is_active, sort_order)",
|
||
"CREATE INDEX IF NOT EXISTS idx_theme_chain_knowledge_theme ON theme_chain_knowledge(theme_name, is_active, sort_order)",
|
||
"CREATE INDEX IF NOT EXISTS idx_scan_process_session_time ON scan_process_logs(scan_session, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_scan_process_stage_time ON scan_process_logs(stage, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_research_observations_session_score ON research_observations(scan_session, final_score)",
|
||
"CREATE INDEX IF NOT EXISTS idx_research_observations_code_time ON research_observations(ts_code, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_research_observations_theme_time ON research_observations(theme_name, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_research_reports_session_time ON research_reports(scan_session, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_research_reports_trade_date ON research_reports(trade_date, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_theme_maps_trade_date_score ON theme_maps(trade_date, heat_score)",
|
||
"CREATE INDEX IF NOT EXISTS idx_theme_chain_theme ON theme_chain_nodes(theme_name, trade_date)",
|
||
"CREATE INDEX IF NOT EXISTS idx_stock_research_code_time ON stock_research_notes(ts_code, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_risk_events_session_reject ON risk_events(scan_session, reject)",
|
||
"CREATE INDEX IF NOT EXISTS idx_opportunity_cards_session_score ON opportunity_cards(scan_session, score)",
|
||
]:
|
||
try:
|
||
await conn.execute(__import__("sqlalchemy").text(index_sql))
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
await conn.execute(
|
||
__import__("sqlalchemy").text(
|
||
"UPDATE users SET email = username WHERE email IS NULL OR email = ''"
|
||
)
|
||
)
|
||
except Exception:
|
||
pass
|