调整部分前端布局和后端增删改逻辑
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .service import DATABASE_RUNTIME, DatabaseRuntimeService
|
||||
|
||||
__all__ = ['DATABASE_RUNTIME', 'DatabaseRuntimeService']
|
||||
@@ -0,0 +1,56 @@
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
|
||||
class DatabaseInfrastructureGateway:
|
||||
"""
|
||||
数据库基础设施网关。
|
||||
|
||||
该对象负责延迟加载数据库引擎、SQLAlchemy 和 Alembic 相关依赖,
|
||||
供数据库运行时 facade 与其协作对象统一复用。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_async_db_engine_factory() -> Any:
|
||||
"""
|
||||
获取异步数据库引擎工厂函数。
|
||||
|
||||
:return: 异步数据库引擎工厂函数
|
||||
"""
|
||||
return import_module('config.database').create_async_db_engine
|
||||
|
||||
@staticmethod
|
||||
def get_sync_db_engine_factory() -> Any:
|
||||
"""
|
||||
获取同步数据库引擎工厂函数。
|
||||
|
||||
:return: 同步数据库引擎工厂函数
|
||||
"""
|
||||
return import_module('config.database').create_sync_db_engine
|
||||
|
||||
@staticmethod
|
||||
def get_sqlalchemy_text() -> Any:
|
||||
"""
|
||||
获取 SQLAlchemy `text` 构造函数。
|
||||
|
||||
:return: SQLAlchemy `text` 函数
|
||||
"""
|
||||
return import_module('sqlalchemy').text
|
||||
|
||||
@staticmethod
|
||||
def get_alembic_config_class() -> Any:
|
||||
"""
|
||||
获取 Alembic 配置类。
|
||||
|
||||
:return: Alembic 配置类
|
||||
"""
|
||||
return import_module('alembic.config').Config
|
||||
|
||||
@staticmethod
|
||||
def get_alembic_script_directory_class() -> Any:
|
||||
"""
|
||||
获取 Alembic 脚本目录类。
|
||||
|
||||
:return: Alembic 脚本目录类
|
||||
"""
|
||||
return import_module('alembic.script').ScriptDirectory
|
||||
@@ -0,0 +1,199 @@
|
||||
from typing import Any
|
||||
|
||||
from cli.exit_codes import DATABASE_ERROR
|
||||
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
|
||||
|
||||
from .gateway import DatabaseInfrastructureGateway
|
||||
from .support import DatabaseAlembicCommandSupport, DatabaseRevisionSupport
|
||||
|
||||
|
||||
class DatabaseRuntimeService:
|
||||
"""
|
||||
数据库运行时服务。
|
||||
|
||||
该服务作为数据库运行时 facade,对外统一暴露数据库连通性检查、
|
||||
Alembic 版本读取与迁移命令执行入口。
|
||||
|
||||
:param runtime_environment: 运行时环境服务
|
||||
:param infrastructure_gateway: 数据库基础设施网关
|
||||
:param revision_support: 数据库迁移版本支持对象
|
||||
:param alembic_command_support: 数据库 Alembic 命令支持对象
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
runtime_environment: RuntimeEnvironmentService | None = None,
|
||||
infrastructure_gateway: DatabaseInfrastructureGateway | None = None,
|
||||
revision_support: DatabaseRevisionSupport | None = None,
|
||||
alembic_command_support: DatabaseAlembicCommandSupport | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
初始化数据库运行时服务。
|
||||
|
||||
:param runtime_environment: 运行时环境服务
|
||||
:param infrastructure_gateway: 数据库基础设施网关
|
||||
:param revision_support: 数据库迁移版本支持对象
|
||||
:param alembic_command_support: 数据库 Alembic 命令支持对象
|
||||
:return: None
|
||||
"""
|
||||
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
|
||||
self.infrastructure_gateway = infrastructure_gateway or DatabaseInfrastructureGateway()
|
||||
self.revision_support = revision_support or DatabaseRevisionSupport(
|
||||
self.infrastructure_gateway,
|
||||
self.runtime_environment,
|
||||
)
|
||||
self.alembic_command_support = alembic_command_support or DatabaseAlembicCommandSupport(
|
||||
self.runtime_environment
|
||||
)
|
||||
|
||||
async def ping_database(self) -> dict[str, Any]:
|
||||
"""
|
||||
检查数据库连通性。
|
||||
|
||||
:return: 数据库检查结果
|
||||
"""
|
||||
create_async_db_engine = self.infrastructure_gateway.get_async_db_engine_factory()
|
||||
text = self.infrastructure_gateway.get_sqlalchemy_text()
|
||||
engine = create_async_db_engine(echo=False)
|
||||
try:
|
||||
async with engine.connect() as connection:
|
||||
await connection.execute(text('SELECT 1'))
|
||||
return {'ok': True, 'message': '数据库连接成功'}
|
||||
except Exception as exc:
|
||||
return {'ok': False, 'message': '数据库连接失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
def get_current_revision(self) -> dict[str, Any]:
|
||||
"""
|
||||
获取数据库当前迁移版本。
|
||||
|
||||
:return: 当前迁移版本信息
|
||||
"""
|
||||
create_sync_db_engine = self.infrastructure_gateway.get_sync_db_engine_factory()
|
||||
text = self.infrastructure_gateway.get_sqlalchemy_text()
|
||||
engine = create_sync_db_engine(echo=False)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
revision = connection.execute(text('SELECT version_num FROM alembic_version')).scalar()
|
||||
return {'ok': True, 'currentRevision': revision}
|
||||
except Exception as exc:
|
||||
return {'ok': False, 'message': '读取数据库迁移版本失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def upgrade_database(self, revision: str = 'head', *, dry_run: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
执行数据库迁移升级。
|
||||
|
||||
:param revision: 目标迁移版本,默认为 `head`
|
||||
:param dry_run: 是否仅演练执行
|
||||
:return: 数据库迁移执行结果
|
||||
"""
|
||||
return self.alembic_command_support.run_alembic_command(
|
||||
'upgrade',
|
||||
revision,
|
||||
success_message=f'数据库已升级到 {revision}',
|
||||
failure_message='数据库升级失败',
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
def init_database(self, *, dry_run: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
初始化数据库到最新迁移版本。
|
||||
|
||||
:param dry_run: 是否仅演练执行
|
||||
:return: 数据库初始化结果
|
||||
"""
|
||||
return self.alembic_command_support.run_alembic_command(
|
||||
'upgrade',
|
||||
'head',
|
||||
success_message='数据库初始化完成,当前版本已同步到 head',
|
||||
failure_message='数据库初始化失败',
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
def downgrade_database(self, revision: str = '-1', *, dry_run: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
执行数据库回退。
|
||||
|
||||
:param revision: 目标回退版本,默认为 `-1`
|
||||
:param dry_run: 是否仅演练执行
|
||||
:return: 数据库回退结果
|
||||
"""
|
||||
return self.alembic_command_support.run_alembic_command(
|
||||
'downgrade',
|
||||
revision,
|
||||
success_message=f'数据库已回退到 {revision}',
|
||||
failure_message='数据库回退失败',
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
def create_revision(self, message: str, *, autogenerate: bool = False, dry_run: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
创建新的 Alembic 迁移版本文件。
|
||||
|
||||
:param message: 迁移说明
|
||||
:param autogenerate: 是否自动生成变更
|
||||
:param dry_run: 是否仅演练执行
|
||||
:return: 迁移版本创建结果
|
||||
"""
|
||||
arguments: list[str] = ['-m', message]
|
||||
if autogenerate:
|
||||
arguments.append('--autogenerate')
|
||||
return self.alembic_command_support.run_alembic_command(
|
||||
'revision',
|
||||
*arguments,
|
||||
success_message='数据库迁移版本文件创建完成',
|
||||
failure_message='数据库迁移版本文件创建失败',
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
def get_alembic_heads(self) -> dict[str, Any]:
|
||||
"""
|
||||
读取当前代码仓库中的 Alembic heads 信息。
|
||||
|
||||
:return: Alembic heads 结果
|
||||
"""
|
||||
try:
|
||||
script_directory = self.revision_support.build_alembic_script_directory()
|
||||
items = [
|
||||
self.revision_support.serialize_revision(revision)
|
||||
for revision in script_directory.get_revisions('heads')
|
||||
]
|
||||
return {
|
||||
'ok': True,
|
||||
'message': '已读取 Alembic heads',
|
||||
'count': len(items),
|
||||
'items': items,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {'ok': False, 'message': '读取 Alembic heads 失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
||||
|
||||
def get_alembic_history(self, *, limit: int = 20) -> dict[str, Any]:
|
||||
"""
|
||||
读取当前代码仓库中的 Alembic 历史版本信息。
|
||||
|
||||
:param limit: 返回的最大历史记录数量
|
||||
:return: Alembic 历史版本结果
|
||||
"""
|
||||
try:
|
||||
script_directory = self.revision_support.build_alembic_script_directory()
|
||||
history_items = [
|
||||
self.revision_support.serialize_revision(revision) for revision in script_directory.walk_revisions()
|
||||
]
|
||||
limited_items = history_items[:limit]
|
||||
return {
|
||||
'ok': True,
|
||||
'message': '已读取 Alembic 历史版本',
|
||||
'count': len(limited_items),
|
||||
'totalCount': len(history_items),
|
||||
'limit': limit,
|
||||
'items': limited_items,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {'ok': False, 'message': '读取 Alembic 历史版本失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
||||
|
||||
|
||||
DATABASE_RUNTIME = DatabaseRuntimeService()
|
||||
@@ -0,0 +1,160 @@
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from cli.exit_codes import DATABASE_ERROR
|
||||
from cli.runtime.base import RuntimeEnvironmentService
|
||||
|
||||
from .gateway import DatabaseInfrastructureGateway
|
||||
|
||||
|
||||
class DatabaseRevisionSupport:
|
||||
"""
|
||||
数据库迁移版本支持对象。
|
||||
|
||||
该对象负责 Alembic script directory 构建、修订版本规整和
|
||||
版本对象序列化,避免主运行时服务继续承载迁移元数据细节。
|
||||
|
||||
:param infrastructure_gateway: 数据库基础设施网关
|
||||
:param runtime_environment: 运行时环境服务
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
infrastructure_gateway: DatabaseInfrastructureGateway,
|
||||
runtime_environment: RuntimeEnvironmentService,
|
||||
) -> None:
|
||||
"""
|
||||
初始化数据库迁移版本支持对象。
|
||||
|
||||
:param infrastructure_gateway: 数据库基础设施网关
|
||||
:param runtime_environment: 运行时环境服务
|
||||
:return: None
|
||||
"""
|
||||
self.infrastructure_gateway = infrastructure_gateway
|
||||
self.runtime_environment = runtime_environment
|
||||
|
||||
def build_alembic_script_directory(self) -> Any:
|
||||
"""
|
||||
基于当前后端目录构建 Alembic 脚本目录对象。
|
||||
|
||||
:return: Alembic 脚本目录对象
|
||||
"""
|
||||
config_class = self.infrastructure_gateway.get_alembic_config_class()
|
||||
script_directory_class = self.infrastructure_gateway.get_alembic_script_directory_class()
|
||||
config = config_class(os.path.join(self.runtime_environment.get_backend_dir(), 'alembic.ini'))
|
||||
return script_directory_class.from_config(config)
|
||||
|
||||
@staticmethod
|
||||
def normalize_revision_value(value: Any) -> list[str]:
|
||||
"""
|
||||
规范化 Alembic 修订版本引用值。
|
||||
|
||||
:param value: 原始修订版本引用
|
||||
:return: 规范化后的修订版本列表
|
||||
"""
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, str):
|
||||
return [value]
|
||||
return [str(item) for item in value]
|
||||
|
||||
def serialize_revision(self, script_revision: Any) -> dict[str, Any]:
|
||||
"""
|
||||
序列化 Alembic 修订版本对象。
|
||||
|
||||
:param script_revision: Alembic 修订版本对象
|
||||
:return: 序列化后的修订版本字典
|
||||
"""
|
||||
return {
|
||||
'revision': str(script_revision.revision),
|
||||
'downRevisions': self.normalize_revision_value(script_revision.down_revision),
|
||||
'branchLabels': sorted(str(item) for item in script_revision.branch_labels or []),
|
||||
'dependsOn': self.normalize_revision_value(script_revision.dependencies),
|
||||
'doc': (script_revision.doc or '').strip(),
|
||||
'path': str(script_revision.path),
|
||||
}
|
||||
|
||||
|
||||
class DatabaseAlembicCommandSupport:
|
||||
"""
|
||||
数据库 Alembic 命令支持对象。
|
||||
|
||||
该对象负责 Alembic 命令组装与子进程执行,
|
||||
避免主运行时服务继续承载命令行桥接细节。
|
||||
|
||||
:param runtime_environment: 运行时环境服务
|
||||
"""
|
||||
|
||||
def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None:
|
||||
"""
|
||||
初始化数据库 Alembic 命令支持对象。
|
||||
|
||||
:param runtime_environment: 运行时环境服务
|
||||
:return: None
|
||||
"""
|
||||
self.runtime_environment = runtime_environment
|
||||
|
||||
def build_alembic_command(self, command: str, *arguments: str) -> list[str]:
|
||||
"""
|
||||
构建 Alembic 命令。
|
||||
|
||||
:param command: Alembic 子命令名称
|
||||
:param arguments: Alembic 子命令参数列表
|
||||
:return: Alembic 命令参数列表
|
||||
"""
|
||||
alembic_ini_path = os.path.join(self.runtime_environment.get_backend_dir(), 'alembic.ini')
|
||||
return ['alembic', '-c', alembic_ini_path, command, *arguments]
|
||||
|
||||
def run_alembic_command(
|
||||
self,
|
||||
command: str,
|
||||
*arguments: str,
|
||||
success_message: str,
|
||||
failure_message: str,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
执行 Alembic 命令并返回统一结果。
|
||||
|
||||
:param command: Alembic 子命令名称
|
||||
:param arguments: Alembic 子命令参数列表
|
||||
:param success_message: 成功提示信息
|
||||
:param failure_message: 失败提示信息
|
||||
:param dry_run: 是否仅演练执行
|
||||
:return: Alembic 执行结果
|
||||
"""
|
||||
command_arguments = self.build_alembic_command(command, *arguments)
|
||||
if dry_run:
|
||||
return {
|
||||
'ok': True,
|
||||
'message': f'{success_message}(dry-run)',
|
||||
'dryRun': True,
|
||||
'command': command_arguments,
|
||||
'cwd': self.runtime_environment.get_backend_dir(),
|
||||
}
|
||||
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command_arguments,
|
||||
cwd=self.runtime_environment.get_backend_dir(),
|
||||
env={**os.environ, 'APP_ENV': os.environ.get('APP_ENV', 'dev')},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
return {
|
||||
'ok': False,
|
||||
'message': failure_message,
|
||||
'error': completed.stderr.strip() or completed.stdout.strip(),
|
||||
'exit_code': DATABASE_ERROR,
|
||||
}
|
||||
|
||||
payload: dict[str, Any] = {'ok': True, 'message': success_message}
|
||||
stdout = completed.stdout.strip()
|
||||
if stdout:
|
||||
payload['stdout'] = stdout
|
||||
return payload
|
||||
except Exception as exc:
|
||||
return {'ok': False, 'message': failure_message, 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
||||
Reference in New Issue
Block a user