176bafdfe5
- db.py:原生 sqlite3 模块重写为 SQLAlchemy 同步引擎(经 infrastructure.make_sync_engine, 同源 config.DATABASE_URL,SQLite/MySQL 双方言);保留 get_conn/fetch_one/list_all/ insert/update_row/delete_row 签名,get_conn 返回「? 占位符兼容 shim」,20+ 调用点零改动 - SCHEMA 常量存档不再执行(表由 alembic 0003+ 管理);新增 reset_engine 供测试/切库 - main.py:删除 sqlite3 直连残余(未支付订单关闭路径)
202 lines
5.9 KiB
Python
202 lines
5.9 KiB
Python
# -*- 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})
|