```
feat(feishu): 添加飞书入站事件inbox和混合数据库协调功能 - 实现飞书入站事件持久化inbox机制,支持状态管理、租约锁定和重试退避 - 添加混合数据库基线协调工具,确保平台PostgreSQL结构安全对齐 - 增加运行组件心跳检测和readiness就绪检查机制 - 实现app_ticket事件的安全轮换和验证处理 - 添加生产环境运行编排和fail-closed安全机制 - 支持webhook快速确认和长连接独立进程处理 - 完善个人数据擦除时的待处理事件清理功能 ```
This commit is contained in:
26
app/core/database/migrations.py
Normal file
26
app/core/database/migrations.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from pathlib import Path
|
||||
|
||||
from alembic.config import Config
|
||||
from alembic.script import ScriptDirectory
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.engine import Connection
|
||||
|
||||
|
||||
def expected_alembic_heads(project_root: Path | None = None) -> tuple[str, ...]:
|
||||
"""Return the repository's configured Alembic heads."""
|
||||
|
||||
root = project_root or Path(__file__).resolve().parents[3]
|
||||
config = Config(str(root / "alembic.ini"))
|
||||
config.set_main_option("script_location", str(root / "alembic"))
|
||||
return tuple(sorted(ScriptDirectory.from_config(config).get_heads()))
|
||||
|
||||
|
||||
def current_alembic_revisions(connection: Connection) -> tuple[str, ...] | None:
|
||||
"""Return database revisions, or ``None`` when it was never versioned."""
|
||||
|
||||
if "alembic_version" not in inspect(connection).get_table_names():
|
||||
return None
|
||||
revisions = connection.execute(
|
||||
text("SELECT version_num FROM alembic_version ORDER BY version_num")
|
||||
).scalars()
|
||||
return tuple(str(revision) for revision in revisions)
|
||||
62
app/core/database/safety.py
Normal file
62
app/core/database/safety.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from sqlalchemy.engine import URL, make_url
|
||||
from sqlalchemy.exc import ArgumentError
|
||||
|
||||
|
||||
_DEFAULT_DATABASE_PORTS = {
|
||||
"mysql": 3306,
|
||||
"postgresql": 5432,
|
||||
}
|
||||
_PLATFORM_MIGRATION_BACKENDS = frozenset({"postgresql", "sqlite"})
|
||||
|
||||
DatabaseTarget = tuple[str, str, int | None, str]
|
||||
|
||||
|
||||
def database_target(value: str | URL | None) -> DatabaseTarget | None:
|
||||
"""Return a credential-free physical database identity."""
|
||||
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
url = make_url(value)
|
||||
except (ArgumentError, TypeError, ValueError):
|
||||
return None
|
||||
backend = url.get_backend_name().lower()
|
||||
return (
|
||||
backend,
|
||||
str(url.host or "").casefold(),
|
||||
url.port or _DEFAULT_DATABASE_PORTS.get(backend),
|
||||
str(url.database or ""),
|
||||
)
|
||||
|
||||
|
||||
def database_password(value: str | URL | None) -> str | None:
|
||||
"""Return a configured password without including it in diagnostics."""
|
||||
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
password = make_url(value).password
|
||||
except (ArgumentError, TypeError, ValueError):
|
||||
return None
|
||||
return str(password) if password is not None else None
|
||||
|
||||
|
||||
def validate_platform_migration_target(
|
||||
database_url: str | URL,
|
||||
legacy_database_url: str | None,
|
||||
) -> DatabaseTarget:
|
||||
"""Fail closed before Alembic can connect to an unsafe database target."""
|
||||
|
||||
platform = database_target(database_url)
|
||||
if platform is None:
|
||||
raise RuntimeError("DATABASE_URL is not a valid platform database URL")
|
||||
if platform[0] not in _PLATFORM_MIGRATION_BACKENDS:
|
||||
raise RuntimeError(
|
||||
"Platform migrations are supported only for PostgreSQL or local SQLite"
|
||||
)
|
||||
legacy = database_target(legacy_database_url)
|
||||
if legacy is not None and platform == legacy:
|
||||
raise RuntimeError(
|
||||
"Refusing to migrate DATABASE_URL because it matches LEGACY_DATABASE_URL"
|
||||
)
|
||||
return platform
|
||||
Reference in New Issue
Block a user