Files
main_plugin_farm/core/database/database.py
T

537 lines
16 KiB
Python
Raw Normal View History

2025-11-30 16:45:13 +08:00
from abc import ABC
2025-10-20 23:24:33 +08:00
from contextlib import asynccontextmanager
import os
from pathlib import Path
import re
2025-11-30 16:45:13 +08:00
from typing import Any, ClassVar
2025-10-20 23:24:33 +08:00
import aiosqlite
from zhenxun.services.log import logger
2025-11-30 16:45:13 +08:00
from ...utils import config
from .dbServiceLocator import g_pDBlocator
2025-10-20 23:24:33 +08:00
2025-11-30 16:45:13 +08:00
class CSqlManager(ABC):
"""
数据库管理器基类
所有具体的数据管理器都应该继承这个类
"""
m_pDB: ClassVar[aiosqlite.Connection | None] = None
2025-10-20 23:24:33 +08:00
def __init__(self):
2025-11-30 16:45:13 +08:00
self.m_sTableName: str | None = None
self.m_bInitialized: bool = False
2025-10-20 23:24:33 +08:00
2025-11-30 16:45:13 +08:00
self.m_pDBPath = Path(config.g_sDBPath)
if self.m_pDBPath and not self.m_pDBPath.exists():
os.makedirs(self.m_pDBPath, exist_ok=True)
2025-10-20 23:24:33 +08:00
@classmethod
async def init(cls) -> bool:
try:
2025-11-30 16:45:13 +08:00
if cls.m_pDB is None:
cls.m_pDB = await aiosqlite.connect(config.g_sDBFilePath)
cls.m_pDB.row_factory = aiosqlite.Row
logger.debug("真寻农场数据库连接初始化成功")
2025-11-30 16:45:13 +08:00
return True
else:
logger.debug("真寻农场数据库连接已存在,跳过初始化")
2025-11-30 16:45:13 +08:00
return True
2025-10-20 23:24:33 +08:00
except Exception as e:
2025-11-30 16:45:13 +08:00
logger.error(f"真寻农场数据库连接初始化失败: {e}")
2025-10-20 23:24:33 +08:00
return False
2025-11-30 16:45:13 +08:00
@classmethod
async def cleanup(cls):
try:
if not cls.isConnectionValid():
logger.warning("真寻农场数据库连接已关闭或无效,跳过清理")
return
if hasattr(cls, "m_pDB") and cls.m_pDB:
await cls.m_pDB.close()
cls.m_pDB = None
cls._initialized = False
logger.debug("真寻农场数据库连接已安全关闭")
else:
logger.debug("真寻农场数据库连接已不存在,无需关闭")
except Exception as e:
logger.error(f"真寻农场数据库清理过程中出错: {e}")
2025-11-30 16:45:13 +08:00
cls.m_pDB = None
cls._initialized = False
@classmethod
def isConnectionValid(cls) -> bool:
if not hasattr(cls, "m_pDB") or cls.m_pDB is None:
return False
try:
return hasattr(cls.m_pDB, "_conn") and cls.m_pDB._conn is not None
except:
return False
2025-11-30 16:45:13 +08:00
@classmethod
def getDB(cls) -> aiosqlite.Connection:
if cls.m_pDB is None:
raise RuntimeError("数据库连接未初始化,请先调用 init_global_db()")
return cls.m_pDB
@classmethod
def getPlantManager(cls) -> Any:
return g_pDBlocator.getPlantManager()
@classmethod
def getUserManager(cls) -> Any:
return g_pDBlocator.getUserManager()
@classmethod
def getUserSoilManager(cls) -> Any:
return g_pDBlocator.getUserSoilManager()
@classmethod
def getUserSeedManager(cls) -> Any:
return g_pDBlocator.getUserSeedManager()
@classmethod
def getUserItemManager(cls) -> Any:
return g_pDBlocator.getUserItemManager()
@classmethod
def getUserStealManager(cls) -> Any:
return g_pDBlocator.getUserStealManager()
@classmethod
def getUserSignManager(cls) -> Any:
return g_pDBlocator.getUserSignManager()
@classmethod
def getUserPlantCountManager(cls) -> Any:
return g_pDBlocator.getUserPlantCountManager()
def isInitialized(self) -> bool:
"""检查是否已初始化"""
return self.m_bInitialized
def setInitialized(self) -> None:
"""标记为已初始化"""
self.m_bInitialized = True
2025-10-20 23:24:33 +08:00
@classmethod
@asynccontextmanager
async def _transaction(cls):
2025-11-30 16:45:13 +08:00
await cls.getDB().execute("BEGIN;")
2025-10-20 23:24:33 +08:00
try:
yield
except:
2025-11-30 16:45:13 +08:00
await cls.getDB().execute("ROLLBACK;")
2025-10-20 23:24:33 +08:00
raise
else:
2025-11-30 16:45:13 +08:00
await cls.getDB().execute("COMMIT;")
2025-10-20 23:24:33 +08:00
@classmethod
async def getTableInfo(cls, tableName: str) -> list:
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", tableName):
raise ValueError(f"Illegal table name: {tableName}")
try:
2025-11-30 16:45:13 +08:00
cursor = await cls.getDB().execute(f'PRAGMA table_info("{tableName}")')
2025-10-20 23:24:33 +08:00
rows = await cursor.fetchall()
return [{"name": row[1], "type": row[2]} for row in rows]
except aiosqlite.Error:
return []
2025-11-30 16:45:13 +08:00
# @classmethod
# async def ensureTableSchema(cls, tableName: str, columns: dict) -> bool:
# """由AI生成
# 创建表或为已存在表添加缺失字段。
# 返回 True 表示有变更(创建或新增列),False 则无操作
# Args:
# tableName (str): 表名
# columns (dict): 字典
# Returns:
# bool:
# """
# info = await cls.getTableInfo(tableName)
# existing = {col["name"]: col["type"].upper() for col in info}
# desired = {k: v.upper() for k, v in columns.items() if k != "PRIMARY KEY"}
# primaryKey = columns.get("PRIMARY KEY", "")
# if not existing:
# colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
# if primaryKey:
# colsDef += f", PRIMARY KEY {primaryKey}"
# await cls.getDB().execute(f'CREATE TABLE "{tableName}" ({colsDef});')
# return True
# toAdd = [k for k in desired if k not in existing]
# toRemove = [k for k in existing if k not in desired]
# typeMismatch = [
# k for k in desired if k in existing and existing[k] != desired[k]
# ]
# if toAdd and not toRemove and not typeMismatch:
# for col in toAdd:
# await cls.getDB().execute(
# f'ALTER TABLE "{tableName}" ADD COLUMN "{col}" {columns[col]}'
# )
# return True
# async with cls._transaction():
# tmpTable = f"{tableName}_new"
# colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
# if primaryKey:
# colsDef += f", PRIMARY KEY {primaryKey}"
# await cls.getDB().execute(f'CREATE TABLE "{tmpTable}" ({colsDef});')
# commonCols = [k for k in desired if k in existing]
# if commonCols:
# colsStr = ", ".join(f'"{c}"' for c in commonCols)
# sql = (
# f'INSERT INTO "{tmpTable}" ({colsStr}) '
# f"SELECT {colsStr} "
# f'FROM "{tableName}";'
# )
# await cls.getDB().execute(sql)
# await cls.getDB().execute(f'DROP TABLE "{tableName}";')
# await cls.getDB().execute(
# f'ALTER TABLE "{tmpTable}" RENAME TO "{tableName}";'
# )
# return True
2025-10-20 23:24:33 +08:00
@classmethod
async def ensureTableSchema(cls, tableName: str, columns: dict) -> bool:
"""由AI生成
创建表或为已存在表添加缺失字段。
返回 True 表示有变更(创建或新增列),False 则无操作
Args:
2025-11-30 16:45:13 +08:00
tableName (str): 表名
columns (dict): 字典
2025-10-20 23:24:33 +08:00
Returns:
2025-11-30 16:45:13 +08:00
bool:
2025-10-20 23:24:33 +08:00
"""
info = await cls.getTableInfo(tableName)
existing = {col["name"]: col["type"].upper() for col in info}
desired = {k: v.upper() for k, v in columns.items() if k != "PRIMARY KEY"}
primaryKey = columns.get("PRIMARY KEY", "")
if not existing:
colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
if primaryKey:
colsDef += f", PRIMARY KEY {primaryKey}"
2025-11-30 16:45:13 +08:00
await cls.getDB().execute(f'CREATE TABLE "{tableName}" ({colsDef});')
2025-10-20 23:24:33 +08:00
return True
toAdd = [k for k in desired if k not in existing]
toRemove = [k for k in existing if k not in desired]
typeMismatch = [
k for k in desired if k in existing and existing[k] != desired[k]
]
2025-11-30 16:45:13 +08:00
if toRemove or typeMismatch:
return False
if toAdd:
2025-10-20 23:24:33 +08:00
for col in toAdd:
2025-11-30 16:45:13 +08:00
try:
await cls.getDB().execute(
f'ALTER TABLE "{tableName}" ADD COLUMN "{col}" {columns[col]}'
)
except aiosqlite.OperationalError as e:
if "duplicate column name" in str(e).lower():
continue
else:
raise
2025-10-20 23:24:33 +08:00
return True
2025-11-30 16:45:13 +08:00
return False
2025-10-20 23:24:33 +08:00
@classmethod
async def executeDB(cls, command: str) -> bool:
"""执行自定义SQL
Args:
command (str): SQL语句
Returns:
bool: 是否执行成功
"""
if not command:
return False
try:
async with cls._transaction():
2025-11-30 16:45:13 +08:00
await cls.getDB().execute(command)
2025-10-20 23:24:33 +08:00
return True
except Exception:
return False
@classmethod
async def insert(cls, tableName: str, data: dict) -> bool:
"""
插入数据
Args:
tableName: 表名
data: 要插入的数据字典,键为字段名,值为字段值
Returns:
bool: 是否执行成功
"""
if not data:
return False
try:
# 构建参数化查询
columns = ", ".join(f'"{k}"' for k in data.keys())
placeholders = ", ".join("?" for _ in data.keys())
values = list(data.values())
sql = f'INSERT INTO "{tableName}" ({columns}) VALUES ({placeholders})'
async with cls._transaction():
2025-11-30 16:45:13 +08:00
await cls.getDB().execute(sql, values)
2025-10-20 23:24:33 +08:00
return True
except Exception as e:
logger.debug("真寻农场插入数据失败!", e=e)
return False
@classmethod
async def batch_insert(cls, tableName: str, data_list: list) -> bool:
"""
批量插入数据
Args:
tableName: 表名
data_list: 要插入的数据字典列表
Returns:
bool: 是否执行成功
"""
if not data_list:
return False
try:
# 使用第一个字典的键作为所有记录的字段
columns = ", ".join(f'"{k}"' for k in data_list[0].keys())
placeholders = ", ".join("?" for _ in data_list[0].keys())
sql = f'INSERT INTO "{tableName}" ({columns}) VALUES ({placeholders})'
async with cls._transaction():
2025-11-30 16:45:13 +08:00
await cls.getDB().executemany(
2025-10-20 23:24:33 +08:00
sql, [list(data.values()) for data in data_list]
)
return True
except Exception as e:
logger.debug("真寻农场批量插入数据失败!", e=e)
return False
@classmethod
async def select(
cls,
tableName: str,
columns: list[Any] | None = None,
where: dict[str, Any] | None = None,
order_by: str | None = None,
limit: int | None = None,
) -> list[dict]:
"""
查询数据
Args:
tableName: 表名
columns: 要查询的字段列表,None表示所有字段
where: 查询条件字典
order_by: 排序字段
limit: 限制返回记录数
Returns:
list: 查询结果列表,每个元素是一个字典
"""
try:
# 构建SELECT部分
if columns:
select_clause = ", ".join(f'"{col}"' for col in columns)
else:
select_clause = "*"
sql = f'SELECT {select_clause} FROM "{tableName}"'
# 构建WHERE部分
params = []
if where:
where_conditions = []
for key, value in where.items():
if isinstance(value, (list, tuple)):
# 处理IN查询
placeholders = ", ".join("?" for _ in value)
where_conditions.append(f'"{key}" IN ({placeholders})')
params.extend(value)
else:
where_conditions.append(f'"{key}" = ?')
params.append(value)
if where_conditions:
sql += " WHERE " + " AND ".join(where_conditions)
# 构建ORDER BY部分
if order_by:
sql += f" ORDER BY {order_by}"
# 构建LIMIT部分
if limit:
sql += f" LIMIT {limit}"
2025-11-30 16:45:13 +08:00
cursor = await cls.getDB().execute(sql, params)
2025-10-20 23:24:33 +08:00
rows = await cursor.fetchall()
# 转换为字典列表
result = []
for row in rows:
result.append(dict(row))
return result
except Exception as e:
logger.debug("真寻农场查询数据失败!", e=e)
return []
@classmethod
async def update(cls, tableName: str, data: dict, where: dict) -> bool:
"""
更新数据
Args:
tableName: 表名
data: 要更新的数据字典
where: 更新条件字典
Returns:
bool: 是否执行成功
"""
if not data:
return False
if not where:
return False
try:
# 构建SET部分
set_conditions = []
params = []
for key, value in data.items():
set_conditions.append(f'"{key}" = ?')
params.append(value)
# 构建WHERE部分
where_conditions = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql = f'UPDATE "{tableName}" SET {", ".join(set_conditions)} WHERE {" AND ".join(where_conditions)}'
async with cls._transaction():
2025-11-30 16:45:13 +08:00
cursor = await cls.getDB().execute(sql, params)
2025-10-20 23:24:33 +08:00
# 检查是否影响了行
return cursor.rowcount > 0
except Exception as e:
logger.debug("真寻农场更新数据失败!", e=e)
return False
@classmethod
async def delete(cls, tableName: str, where: dict) -> bool:
"""
删除数据
Args:
tableName: 表名
where: 删除条件字典
Returns:
bool: 是否执行成功
"""
if not where:
return False
try:
# 构建WHERE部分
where_conditions = []
params = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql = f'DELETE FROM "{tableName}" WHERE {" AND ".join(where_conditions)}'
async with cls._transaction():
2025-11-30 16:45:13 +08:00
cursor = await cls.getDB().execute(sql, params)
2025-10-20 23:24:33 +08:00
# 检查是否影响了行
return cursor.rowcount > 0
except Exception as e:
logger.debug("真寻农场删除数据失败!", e=e)
return False
@classmethod
async def exists(cls, tableName: str, where: dict) -> bool:
"""
检查记录是否存在
Args:
tableName: 表名
where: 查询条件字典
Returns:
bool: 是否存在符合条件的记录
"""
try:
result = await cls.select(tableName, columns=["1"], where=where, limit=1)
return len(result) > 0
except Exception as e:
logger.debug("真寻农场检查数据失败!", e=e)
return False
@classmethod
async def count(cls, tableName: str, where: dict = {}) -> int:
"""
统计记录数量
Args:
tableName: 表名
where: 查询条件字典
Returns:
int: 记录数量
"""
try:
# 构建WHERE部分
sql = f'SELECT COUNT(*) as count FROM "{tableName}"'
params = []
if where:
where_conditions = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql += " WHERE " + " AND ".join(where_conditions)
2025-11-30 16:45:13 +08:00
cursor = await cls.getDB().execute(sql, params)
2025-10-20 23:24:33 +08:00
row = await cursor.fetchone()
return row["count"] if row else 0
except Exception as e:
logger.debug("真寻农场统计数据失败!", e=e)
return 0
g_pSqlManager = CSqlManager()