```
feat: 添加数据库迁移脚本并更新Dockerfile配置 - 在Dockerfile中添加alembic配置文件和目录的复制指令 - 更新alembic/env.py注册新的模块模型:events、workflows、writebacks - 生成完整的初始数据库schema迁移脚本,包含以下表: - approval_requests, attendance_records, audit_logs, domain_events - expenses, feishu_event_receipts, fund_accounts, legacy_sync_runs - official_writeback_runs, performance_metrics, policies, procurements - projects, report_push_runs, risk_event_actions, risk_events - standards, suppliers, work_reports, work_tasks, workflow_actions - workflow_instances等21个数据表结构定义 - 在API路由器中添加新模块的路由:events、workflows、writebacks、observability ```
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"""Add V3 enterprise automation foundation tables.
|
||||
|
||||
Revision ID: 202607080002
|
||||
Revises: 202607080001
|
||||
Create Date: 2026-07-08
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "202607080002"
|
||||
down_revision = "202607080001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
AUDIT_LOGS_TABLE = "audit_logs"
|
||||
DOMAIN_EVENTS_TABLE = "domain_events"
|
||||
WORKFLOW_INSTANCES_TABLE = "workflow_instances"
|
||||
WORKFLOW_ACTIONS_TABLE = "workflow_actions"
|
||||
OFFICIAL_WRITEBACK_RUNS_TABLE = "official_writeback_runs"
|
||||
|
||||
|
||||
def _table_exists(table_name: str) -> bool:
|
||||
inspector = inspect(op.get_bind())
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def _column_names(table_name: str) -> set[str]:
|
||||
inspector = inspect(op.get_bind())
|
||||
if table_name not in inspector.get_table_names():
|
||||
return set()
|
||||
return {column["name"] for column in inspector.get_columns(table_name)}
|
||||
|
||||
|
||||
def _add_column_if_missing(table_name: str, column: sa.Column) -> None:
|
||||
if column.name not in _column_names(table_name):
|
||||
op.add_column(table_name, column)
|
||||
|
||||
|
||||
def _create_indexes(table_name: str, indexes: list[tuple[str, bool]]) -> None:
|
||||
for column_name, unique in indexes:
|
||||
op.create_index(
|
||||
op.f(f"ix_{table_name}_{column_name}"),
|
||||
table_name,
|
||||
[column_name],
|
||||
unique=unique,
|
||||
if_not_exists=True,
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
_add_column_if_missing(
|
||||
AUDIT_LOGS_TABLE,
|
||||
sa.Column("request_id", sa.String(length=64), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_audit_logs_request_id"),
|
||||
AUDIT_LOGS_TABLE,
|
||||
["request_id"],
|
||||
unique=False,
|
||||
if_not_exists=True,
|
||||
)
|
||||
|
||||
if not _table_exists(DOMAIN_EVENTS_TABLE):
|
||||
op.create_table(
|
||||
DOMAIN_EVENTS_TABLE,
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("event_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("event_type", sa.String(length=128), nullable=False),
|
||||
sa.Column("source", sa.String(length=64), nullable=False),
|
||||
sa.Column("aggregate_type", sa.String(length=128), nullable=False),
|
||||
sa.Column("aggregate_id", sa.String(length=128), nullable=True),
|
||||
sa.Column("actor", sa.String(length=128), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=True),
|
||||
sa.Column("status", sa.String(length=32), nullable=False),
|
||||
sa.Column("attempts", sa.Integer(), nullable=False),
|
||||
sa.Column("idempotency_key", sa.String(length=255), nullable=True),
|
||||
sa.Column("last_error", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("processed_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
_create_indexes(
|
||||
DOMAIN_EVENTS_TABLE,
|
||||
[
|
||||
("event_id", True),
|
||||
("event_type", False),
|
||||
("source", False),
|
||||
("aggregate_type", False),
|
||||
("aggregate_id", False),
|
||||
("actor", False),
|
||||
("status", False),
|
||||
("idempotency_key", True),
|
||||
("created_at", False),
|
||||
("processed_at", False),
|
||||
],
|
||||
)
|
||||
|
||||
if not _table_exists(WORKFLOW_INSTANCES_TABLE):
|
||||
op.create_table(
|
||||
WORKFLOW_INSTANCES_TABLE,
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("code", sa.String(length=64), nullable=False),
|
||||
sa.Column("workflow_type", sa.String(length=128), nullable=False),
|
||||
sa.Column("aggregate_type", sa.String(length=128), nullable=False),
|
||||
sa.Column("aggregate_id", sa.String(length=128), nullable=True),
|
||||
sa.Column("status", sa.String(length=32), nullable=False),
|
||||
sa.Column("actor", sa.String(length=128), nullable=False),
|
||||
sa.Column("current_step", sa.String(length=128), nullable=True),
|
||||
sa.Column("payload", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("completed_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
_create_indexes(
|
||||
WORKFLOW_INSTANCES_TABLE,
|
||||
[
|
||||
("code", True),
|
||||
("workflow_type", False),
|
||||
("aggregate_type", False),
|
||||
("aggregate_id", False),
|
||||
("status", False),
|
||||
("actor", False),
|
||||
("created_at", False),
|
||||
("completed_at", False),
|
||||
],
|
||||
)
|
||||
|
||||
if not _table_exists(WORKFLOW_ACTIONS_TABLE):
|
||||
op.create_table(
|
||||
WORKFLOW_ACTIONS_TABLE,
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("code", sa.String(length=64), nullable=False),
|
||||
sa.Column("workflow_code", sa.String(length=64), nullable=False),
|
||||
sa.Column("action", sa.String(length=128), nullable=False),
|
||||
sa.Column("actor", sa.String(length=128), nullable=False),
|
||||
sa.Column("from_status", sa.String(length=32), nullable=True),
|
||||
sa.Column("to_status", sa.String(length=32), nullable=True),
|
||||
sa.Column("payload", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
_create_indexes(
|
||||
WORKFLOW_ACTIONS_TABLE,
|
||||
[
|
||||
("code", True),
|
||||
("workflow_code", False),
|
||||
("action", False),
|
||||
("actor", False),
|
||||
("created_at", False),
|
||||
],
|
||||
)
|
||||
|
||||
if not _table_exists(OFFICIAL_WRITEBACK_RUNS_TABLE):
|
||||
op.create_table(
|
||||
OFFICIAL_WRITEBACK_RUNS_TABLE,
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("code", sa.String(length=64), nullable=False),
|
||||
sa.Column("domain", sa.String(length=128), nullable=False),
|
||||
sa.Column("record_id", sa.String(length=128), nullable=True),
|
||||
sa.Column("action", sa.String(length=128), nullable=False),
|
||||
sa.Column("actor", sa.String(length=128), nullable=False),
|
||||
sa.Column("status", sa.String(length=32), nullable=False),
|
||||
sa.Column("approval_ticket_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("idempotency_key", sa.String(length=255), nullable=True),
|
||||
sa.Column("request_payload", sa.JSON(), nullable=True),
|
||||
sa.Column("provider_response", sa.JSON(), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("submitted_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("sent_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
_create_indexes(
|
||||
OFFICIAL_WRITEBACK_RUNS_TABLE,
|
||||
[
|
||||
("code", True),
|
||||
("domain", False),
|
||||
("record_id", False),
|
||||
("action", False),
|
||||
("actor", False),
|
||||
("status", False),
|
||||
("approval_ticket_id", False),
|
||||
("idempotency_key", True),
|
||||
("created_at", False),
|
||||
("submitted_at", False),
|
||||
("sent_at", False),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for table_name in [
|
||||
OFFICIAL_WRITEBACK_RUNS_TABLE,
|
||||
WORKFLOW_ACTIONS_TABLE,
|
||||
WORKFLOW_INSTANCES_TABLE,
|
||||
DOMAIN_EVENTS_TABLE,
|
||||
]:
|
||||
if _table_exists(table_name):
|
||||
op.drop_table(table_name)
|
||||
|
||||
if "request_id" in _column_names(AUDIT_LOGS_TABLE):
|
||||
op.drop_index(op.f("ix_audit_logs_request_id"), table_name=AUDIT_LOGS_TABLE)
|
||||
op.drop_column(AUDIT_LOGS_TABLE, "request_id")
|
||||
Reference in New Issue
Block a user