feat: add A-card operations frontend and backend foundation

This commit is contained in:
郑龙捷
2026-09-02 09:54:03 +08:00
parent 9badd3597f
commit ad71259ba5
106 changed files with 12461 additions and 1796 deletions
+20
View File
@@ -0,0 +1,20 @@
# Operations migrations
`model_operations` 使用独立 Alembic 版本线,禁止与平台库 `migrations/`
混用。当前 V0.1 全量基线仍以 DBA 已审核的 SQL 为准:
```text
docs/database/model_operations-完整建表-V0.1.sql
```
后续增量变更命令:
```bash
uv run --package backend alembic -c operations-alembic.ini current
uv run --package backend alembic -c operations-alembic.ini revision --autogenerate -m "change"
uv run --package backend alembic -c operations-alembic.ini upgrade head
```
首个增量 revision 建立前,不要用 Alembic 代替 V0.1 全量建表 SQL。
`env.py` 会忽略尚未纳入 P0 ORM 的已存在业务表,避免 autogenerate
误生成删表操作。
+93
View File
@@ -0,0 +1,93 @@
from __future__ import annotations
import asyncio
from logging.config import fileConfig
from typing import Any
from alembic import context
from common.config import settings
from common.db.models.operations import OperationsBase
from sqlalchemy import Connection, pool
from sqlalchemy.ext.asyncio import async_engine_from_config
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = OperationsBase.metadata
def database_url() -> str:
value = settings.operations_database_url
if not value:
raise RuntimeError("OPERATIONS_DATABASE_URL is required")
return value
def include_object(
obj: Any,
name: str | None,
type_: str,
reflected: bool,
compare_to: Any,
) -> bool:
"""Protect audited ops tables not yet represented in the P0 ORM subset."""
if type_ == "table" and reflected and compare_to is None:
return False
return type_ != "table" or bool(name and name.startswith("ops_"))
def configure_context(*, connection: Connection | None = None) -> None:
options = {
"target_metadata": target_metadata,
"compare_type": True,
"compare_server_default": True,
"include_object": include_object,
}
if connection is None:
context.configure(
url=database_url(),
literal_binds=True,
dialect_opts={"paramstyle": "named"},
**options,
)
else:
context.configure(connection=connection, **options)
def run_migrations_offline() -> None:
configure_context()
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
configure_context(connection=connection)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
section = config.get_section(config.config_ini_section, {})
section["sqlalchemy.url"] = database_url()
connectable = async_engine_from_config(
section,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
try:
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
finally:
await connectable.dispose()
def run_migrations_online() -> None:
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+27
View File
@@ -0,0 +1,27 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision: str = ${repr(up_revision)}
down_revision: str | Sequence[str] | None = ${repr(down_revision)}
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade operations schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade operations schema."""
${downgrades if downgrades else "pass"}