"""数据库访问层。 提供 run_query:读取 .sql 文件、剥离注释、参数化绑定执行、返回 list[dict]。 所有报表查询必须走参数绑定,禁止字符串拼接 SQL(防注入)。 """ from __future__ import annotations import re from pathlib import Path from typing import Any from sqlalchemy import create_engine, text from sqlalchemy.engine import Engine from core.settings import settings _engine: Engine | None = None def get_engine() -> Engine: """惰性创建并缓存 SQLAlchemy engine。""" global _engine if _engine is None: _engine = create_engine( settings.database_url, pool_pre_ping=True, pool_recycle=1800, ) return _engine def load_sql(sql_path: str | Path) -> str: """读取 .sql 文件并剥离行级 `--` 注释,返回纯 SQL 文本。 支持 SQL 文件里自由书写中文注释(便于阅读与 AI 理解)。 仅剥离以 `--` 开头的整行注释(行首可有空白),不处理行内注释, 避免误删含 `--` 的字符串字面量。 """ path = Path(sql_path) if not path.is_absolute(): from core.settings import PROJECT_ROOT path = PROJECT_ROOT / path raw = path.read_text(encoding="utf-8") lines = [] for line in raw.splitlines(): # 去掉行首空白后判断是否为注释行 if re.match(r"\s*--", line): continue lines.append(line) return "\n".join(lines).strip() def run_query(sql_path: str | Path, params: dict[str, Any]) -> list[dict[str, Any]]: """执行参数化查询,返回 list[dict](每行一个 dict,键为列别名)。 :param sql_path: .sql 文件路径(相对项目根或绝对路径)。 :param params: 绑定参数,键名对应 SQL 中的 :name 占位符。 """ sql_text = load_sql(sql_path) engine = get_engine() with engine.connect() as conn: result = conn.execute(text(sql_text), params) cols = list(result.keys()) return [dict(zip(cols, row)) for row in result.fetchall()]