2026-08-31 21:40:31 +08:00
|
|
|
|
# -*- 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 常量仅存档,运行时不再建表)。
|
2026-08-23 22:36:00 +08:00
|
|
|
|
"""
|
|
|
|
|
|
import random
|
2026-08-31 21:40:31 +08:00
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
2026-08-31 21:40:31 +08:00
|
|
|
|
from sqlalchemy import text
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
2026-08-31 21:40:31 +08:00
|
|
|
|
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
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
2026-08-31 21:40:31 +08:00
|
|
|
|
|
|
|
|
|
|
def reset_engine() -> None:
|
|
|
|
|
|
"""测试/切库后重置共享引擎(下次使用时按当前 DATABASE_URL 重建)。"""
|
|
|
|
|
|
global _engine
|
|
|
|
|
|
with _engine_lock:
|
|
|
|
|
|
if _engine is not None:
|
|
|
|
|
|
_engine.dispose()
|
|
|
|
|
|
_engine = None
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 21:40:31 +08:00
|
|
|
|
def now_iso():
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-23 22:36:00 +08:00
|
|
|
|
SCHEMA = """
|
2026-08-31 21:40:31 +08:00
|
|
|
|
-- (存档,运行时不再执行)历史 SQLite SCHEMA;表结构现由 alembic 迁移
|
|
|
|
|
|
-- 0003_training_tables 及后续版本管理,MySQL 下类型由 dialect 钩子映射。
|
2026-08-23 22:36:00 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 21:40:31 +08:00
|
|
|
|
# ---------------- 占位符兼容连接 shim ----------------
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
2026-08-31 21:40:31 +08:00
|
|
|
|
def _translate(sql: str, params) -> tuple[str, dict]:
|
|
|
|
|
|
"""sqlite 风格 ``?`` 占位符 → SQLAlchemy 命名参数(:p0, :p1, ...)。
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
2026-08-31 21:40:31 +08:00
|
|
|
|
本层 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)}
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
2026-08-24 16:57:13 +08:00
|
|
|
|
|
2026-08-31 21:40:31 +08:00
|
|
|
|
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())
|
2026-08-24 16:57:13 +08:00
|
|
|
|
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
|
|
|
|
|
# ---------------- 通用读写 helpers ----------------
|
|
|
|
|
|
|
|
|
|
|
|
def rows_to_list(rows):
|
|
|
|
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_one(table, **kw):
|
2026-08-31 21:40:31 +08:00
|
|
|
|
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
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_by_id(table, rid):
|
|
|
|
|
|
return fetch_one(table, id=rid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_all(table, order_by=None):
|
2026-08-31 21:40:31 +08:00
|
|
|
|
sql = f"SELECT * FROM `{table}`"
|
2026-08-23 22:36:00 +08:00
|
|
|
|
if order_by:
|
|
|
|
|
|
sql += f" ORDER BY {order_by}"
|
2026-08-31 21:40:31 +08:00
|
|
|
|
with _get_engine().connect() as conn:
|
|
|
|
|
|
return [dict(r._mapping) for r in conn.execute(text(sql)).fetchall()]
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def insert(table, data):
|
|
|
|
|
|
keys = list(data.keys())
|
2026-08-31 21:40:31 +08:00
|
|
|
|
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)})
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_row(table, rid, patch):
|
|
|
|
|
|
keys = list(patch.keys())
|
2026-08-31 21:40:31 +08:00
|
|
|
|
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)
|
2026-08-23 22:36:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_row(table, rid):
|
2026-08-31 21:40:31 +08:00
|
|
|
|
with _get_engine().begin() as conn:
|
|
|
|
|
|
conn.execute(text(f"DELETE FROM `{table}` WHERE `id`=:pid"), {"pid": rid})
|