feat: implement backend knowledge management
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
from alembic import context
|
||||
from sqlalchemy import create_engine, pool
|
||||
|
||||
from app import models # noqa: F401
|
||||
from app.core.config import settings
|
||||
from app.db.session import Base
|
||||
|
||||
config = context.config
|
||||
|
||||
|
||||
def run_migrations(connection):
|
||||
context.configure(connection=connection, target_metadata=Base.metadata, compare_type=True)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
raise RuntimeError("迁移需要连接数据库,以检查并保留已有表;请使用 alembic upgrade head")
|
||||
elif config.attributes.get("connection") is not None:
|
||||
run_migrations(config.attributes["connection"])
|
||||
else:
|
||||
engine = create_engine(
|
||||
settings.database_url, poolclass=pool.NullPool, connect_args={"connect_timeout": 5}
|
||||
)
|
||||
with engine.connect() as connection:
|
||||
run_migrations(connection)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""${message}"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Register the original knowledge table without replacing existing data."""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0001_baseline"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
inspector = sa.inspect(bind)
|
||||
if inspector.has_table("knowledge_items"):
|
||||
columns = {column["name"] for column in inspector.get_columns("knowledge_items")}
|
||||
expected = {
|
||||
"id",
|
||||
"title",
|
||||
"category",
|
||||
"content",
|
||||
"source_url",
|
||||
"verification_status",
|
||||
"created_at",
|
||||
}
|
||||
if columns != expected:
|
||||
raise RuntimeError("已有 knowledge_items 结构与基础版本不同,请先核对结构并备份")
|
||||
return
|
||||
op.create_table(
|
||||
"knowledge_items",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("title", sa.String(250), nullable=False),
|
||||
sa.Column("category", sa.String(80), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("source_url", sa.String(1000)),
|
||||
sa.Column("verification_status", sa.String(30), nullable=False),
|
||||
sa.Column(
|
||||
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
|
||||
),
|
||||
)
|
||||
op.create_index("ix_knowledge_items_id", "knowledge_items", ["id"])
|
||||
op.create_index("ix_knowledge_items_title", "knowledge_items", ["title"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("knowledge_items")
|
||||
# 扩展可能由其他项目共享,不在回滚时删除。
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Add source, document, review and audit management."""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0002_catalog"
|
||||
down_revision = "0001_baseline"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def version_columns():
|
||||
return [
|
||||
sa.Column("version", sa.Integer(), server_default="1", nullable=False),
|
||||
sa.Column(
|
||||
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
|
||||
),
|
||||
sa.Column("is_deleted", sa.Boolean(), server_default=sa.false(), nullable=False),
|
||||
]
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"sources",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("name", sa.String(250), nullable=False),
|
||||
sa.Column("source_url", sa.String(1000)),
|
||||
sa.Column("notes", sa.Text(), nullable=False),
|
||||
*version_columns(),
|
||||
)
|
||||
op.create_table(
|
||||
"documents",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("source_id", sa.Integer(), sa.ForeignKey("sources.id"), nullable=False),
|
||||
sa.Column("title", sa.String(250), nullable=False),
|
||||
sa.Column("category", sa.String(80), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("collected_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("notes", sa.Text(), nullable=False),
|
||||
*version_columns(),
|
||||
)
|
||||
op.create_index("ix_documents_source_id", "documents", ["source_id"])
|
||||
op.create_table(
|
||||
"audit_events",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("entity_type", sa.String(30), nullable=False),
|
||||
sa.Column("entity_id", sa.Integer(), nullable=False),
|
||||
sa.Column("action", sa.String(30), nullable=False),
|
||||
sa.Column("actor", sa.String(100)),
|
||||
sa.Column("notes", sa.Text(), nullable=False),
|
||||
sa.Column("snapshot", sa.JSON(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
|
||||
),
|
||||
)
|
||||
op.create_index("ix_audit_events_entity_type", "audit_events", ["entity_type"])
|
||||
op.create_index("ix_audit_events_entity_id", "audit_events", ["entity_id"])
|
||||
for column in version_columns():
|
||||
if column.name != "created_at":
|
||||
op.add_column("knowledge_items", column)
|
||||
for name, kind in [
|
||||
("source_id", sa.Integer()),
|
||||
("document_id", sa.Integer()),
|
||||
("collected_at", sa.DateTime(timezone=True)),
|
||||
("reviewer", sa.String(100)),
|
||||
("reviewed_at", sa.DateTime(timezone=True)),
|
||||
("verification_notes", sa.Text()),
|
||||
]:
|
||||
op.add_column("knowledge_items", sa.Column(name, kind))
|
||||
for name, parent in [("source_id", "sources"), ("document_id", "documents")]:
|
||||
op.create_foreign_key(f"fk_knowledge_{name}", "knowledge_items", parent, [name], ["id"])
|
||||
op.create_index(f"ix_knowledge_items_{name}", "knowledge_items", [name])
|
||||
op.create_check_constraint(
|
||||
"ck_knowledge_verification_status",
|
||||
"knowledge_items",
|
||||
"verification_status IN ('pending', 'verified', 'rejected')",
|
||||
)
|
||||
op.execute("""INSERT INTO audit_events (entity_type, entity_id, action, notes, snapshot)
|
||||
SELECT 'knowledge_items', id, 'import', '保留基础版本数据及原状态',
|
||||
json_build_object('item', row_to_json(k)) FROM knowledge_items k""")
|
||||
op.execute("""UPDATE knowledge_items SET verification_status = 'pending', version = version + 1
|
||||
WHERE verification_status = 'verified'""")
|
||||
op.execute("""INSERT INTO audit_events (entity_type, entity_id, action, notes, snapshot)
|
||||
SELECT 'knowledge_items', id, 'invalidate', '旧版本没有完整审核记录,需要重新核验',
|
||||
json_build_object('item', row_to_json(k)) FROM knowledge_items k WHERE version = 2""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_constraint("ck_knowledge_verification_status", "knowledge_items", type_="check")
|
||||
for name in ["source_id", "document_id"]:
|
||||
op.drop_index(f"ix_knowledge_items_{name}", table_name="knowledge_items")
|
||||
op.drop_constraint(f"fk_knowledge_{name}", "knowledge_items", type_="foreignkey")
|
||||
for name in [
|
||||
"source_id",
|
||||
"document_id",
|
||||
"collected_at",
|
||||
"reviewer",
|
||||
"reviewed_at",
|
||||
"verification_notes",
|
||||
"version",
|
||||
"updated_at",
|
||||
"is_deleted",
|
||||
]:
|
||||
op.drop_column("knowledge_items", name)
|
||||
op.drop_table("audit_events")
|
||||
op.drop_table("documents")
|
||||
op.drop_table("sources")
|
||||
Reference in New Issue
Block a user