# -*- coding: utf-8 -*- """培训子应用数据层(同步 helper,经 asyncio.to_thread 调用)。 统一走平台 DATABASE_URL(config.DATABASE_URL,可配 SQLite / MySQL), 由 infrastructure.db.make_sync_engine 映射同步驱动: sqlite+aiosqlite → sqlite;mysql+asyncmy → mysql+pymysql。 历史遗留:本层最初为原生 ``sqlite3`` 直连(``?`` 占位符),为不改动 20+ 处 调用点,``get_conn()`` 返回一个占位符兼容的连接 shim——SQL 用 ``?`` 书写, 执行前翻译为 SQLAlchemy 命名参数,对 SQLite / MySQL 双方言通用。 表结构统一由 alembic 迁移管理(本文件的 SCHEMA 常量仅存档,运行时不再建表)。 """ import random import threading import time from sqlalchemy import text from ..infrastructure.db import make_sync_engine BASE_DIR_KEY = "serverdata" # 兼容旧注释:数据目录统一由 config.SERVERDATA_DIR 承载 _engine = None _engine_lock = threading.Lock() def _get_engine(): global _engine if _engine is None: with _engine_lock: if _engine is None: _engine = make_sync_engine() return _engine def reset_engine() -> None: """测试/切库后重置共享引擎(下次使用时按当前 DATABASE_URL 重建)。""" global _engine with _engine_lock: if _engine is not None: _engine.dispose() _engine = None def gen_id(prefix): ts = time.time() s = int(ts) # base36 大写,模拟 JS genId def b36(n): if n == 0: return "0" d = "0123456789abcdefghijklmnopqrstuvwxyz" out = "" while n: out = d[n % 36] + out n //= 36 return out return prefix + b36(s).upper() + b36(random.randint(0, 35)).upper() def now_iso(): from datetime import datetime, timezone return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") SCHEMA = """ -- (存档,运行时不再执行)历史 SQLite SCHEMA;表结构现由 alembic 迁移 -- 0003_training_tables 及后续版本管理,MySQL 下类型由 dialect 钩子映射。 """ # ---------------- 占位符兼容连接 shim ---------------- def _translate(sql: str, params) -> tuple[str, dict]: """sqlite 风格 ``?`` 占位符 → SQLAlchemy 命名参数(:p0, :p1, ...)。 本层 SQL 均为受控字面量(无字符串内嵌 ``?``),直接顺序替换安全; dict 参数原样透传。列表/元组包装为 ``(x,)`` 的写法均兼容。 """ if isinstance(params, dict): return sql, params if params is None: params = () if not isinstance(params, (list, tuple)): params = (params,) out, n = [], 0 for ch in sql: if ch == "?": out.append(f":p{n}") n += 1 else: out.append(ch) if n != len(params): raise ValueError(f"占位符数量不匹配: {n} 占位符 vs {len(params)} 参数 | {sql}") return "".join(out), {f"p{i}": v for i, v in enumerate(params)} class _Cursor: """轻量结果游标:fetchone/fetchall 返回 dict(兼容 row["col"] 与 dict(row))。""" def __init__(self, result): self._result = result def _to_dict(self, row): return None if row is None else dict(row._mapping) def fetchone(self): return self._to_dict(self._result.fetchone()) def fetchall(self): return [dict(r._mapping) for r in self._result.fetchall()] def __iter__(self): return iter(self.fetchall()) class _Conn: """sqlite3.Connection 兼容 shim(execute/executemany/commit/close)。""" def __init__(self, conn): self._conn = conn def execute(self, sql, params=()): q, p = _translate(sql, params) return _Cursor(self._conn.execute(text(q), p)) def executemany(self, sql, seq_of_params): cur = None for params in seq_of_params: cur = self.execute(sql, params) return cur def commit(self): self._conn.commit() def rollback(self): self._conn.rollback() def close(self): self._conn.close() @property def in_transaction(self): return self._conn.in_transaction() def get_conn(): """取一条连接(shim)。用完必须 close()(归还连接池)。""" return _Conn(_get_engine().connect()) # ---------------- 通用读写 helpers ---------------- def rows_to_list(rows): return [dict(r) for r in rows] def fetch_one(table, **kw): where = " AND ".join([f"`{k}`=:p{i}" for i, k in enumerate(kw)]) sql = f"SELECT * FROM `{table}`" if where: sql += f" WHERE {where}" with _get_engine().connect() as conn: row = conn.execute(text(sql), {f"p{i}": v for i, v in enumerate(kw.values())}).fetchone() return dict(row._mapping) if row else None def fetch_by_id(table, rid): return fetch_one(table, id=rid) def list_all(table, order_by=None): sql = f"SELECT * FROM `{table}`" if order_by: sql += f" ORDER BY {order_by}" with _get_engine().connect() as conn: return [dict(r._mapping) for r in conn.execute(text(sql)).fetchall()] def insert(table, data): keys = list(data.keys()) cols = ",".join(f"`{k}`" for k in keys) marks = ",".join(f":p{i}" for i in range(len(keys))) sql = f"INSERT INTO `{table}` ({cols}) VALUES ({marks})" with _get_engine().begin() as conn: conn.execute(text(sql), {f"p{i}": data[k] for i, k in enumerate(keys)}) def update_row(table, rid, patch): keys = list(patch.keys()) sets = ",".join(f"`{k}`=:p{i}" for i, k in enumerate(keys)) sql = f"UPDATE `{table}` SET {sets} WHERE `id`=:pid" params = {f"p{i}": patch[k] for i, k in enumerate(keys)} params["pid"] = rid with _get_engine().begin() as conn: conn.execute(text(sql), params) def delete_row(table, rid): with _get_engine().begin() as conn: conn.execute(text(f"DELETE FROM `{table}` WHERE `id`=:pid"), {"pid": rid})