Files
server-core/app/training/db.py
T
Pine 176bafdfe5 refactor(training): 数据层收编 SQLAlchemy 统一走 DATABASE_URL(去 sqlite3 直连)
- 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 直连残余(未支付订单关闭路径)
2026-08-31 21:40:31 +08:00

202 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""培训子应用数据层(同步 helper,经 asyncio.to_thread 调用)。
统一走平台 DATABASE_URLconfig.DATABASE_URL,可配 SQLite / MySQL),
由 infrastructure.db.make_sync_engine 映射同步驱动:
sqlite+aiosqlite → sqlitemysql+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 兼容 shimexecute/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})