diff --git a/backend/alembic/versions/059724556401_add_missing_sentiment_fields.py b/backend/alembic/versions/059724556401_add_missing_sentiment_fields.py deleted file mode 100644 index ea0f334..0000000 --- a/backend/alembic/versions/059724556401_add_missing_sentiment_fields.py +++ /dev/null @@ -1,42 +0,0 @@ -"""add_missing_sentiment_fields - -Revision ID: 059724556401 -Revises: a7b9c1d3ef67 -Create Date: 2026-05-23 17:19:50.789398 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '059724556401' -down_revision: Union[str, Sequence[str], None] = 'a7b9c1d3ef67' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # 添加情感分析字段到 citation_records 表 - op.add_column('citation_records', - sa.Column('sentiment', sa.String(20), nullable=True, - comment='情感倾向: positive / neutral / negative') - ) - op.add_column('citation_records', - sa.Column('sentiment_confidence', sa.Float(), nullable=True, - comment='情感分析置信度 0.0-1.0') - ) - op.add_column('citation_records', - sa.Column('sentiment_key_phrases', sa.JSON(), nullable=True, - comment='关键情感短语列表') - ) - - -def downgrade() -> None: - """Downgrade schema.""" - op.drop_column('citation_records', 'sentiment_key_phrases') - op.drop_column('citation_records', 'sentiment_confidence') - op.drop_column('citation_records', 'sentiment') diff --git a/backend/alembic/versions/488d0bd5ab01_initial_migration.py b/backend/alembic/versions/488d0bd5ab01_initial_migration.py deleted file mode 100644 index fc059db..0000000 --- a/backend/alembic/versions/488d0bd5ab01_initial_migration.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Initial migration - -Revision ID: 488d0bd5ab01 -Revises: -Create Date: 2026-04-22 18:06:46.629263 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = '488d0bd5ab01' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # users table - op.create_table( - 'users', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('email', sa.String(255), nullable=False), - sa.Column('password_hash', sa.String(255), nullable=False), - sa.Column('name', sa.String(100), nullable=True), - sa.Column('plan', sa.String(20), server_default='free', nullable=False), - sa.Column('max_queries', sa.Integer(), server_default='5', nullable=False), - sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - - # queries table - op.create_table( - 'queries', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('keyword', sa.String(200), nullable=False), - sa.Column('target_brand', sa.String(100), nullable=False), - sa.Column('brand_aliases', postgresql.JSONB(), server_default='[]', nullable=False), - sa.Column('platforms', postgresql.JSONB(), server_default='["wenxin", "kimi"]', nullable=False), - sa.Column('frequency', sa.String(20), server_default='weekly', nullable=False), - sa.Column('status', sa.String(20), server_default='active', nullable=False), - sa.Column('last_queried_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('next_query_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE') - ) - op.create_index('idx_queries_user_id', 'queries', ['user_id']) - op.create_index('idx_queries_status', 'queries', ['status']) - op.create_index('idx_queries_next_query_at', 'queries', ['next_query_at']) - - # citation_records table - op.create_table( - 'citation_records', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('query_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('platform', sa.String(50), nullable=False), - sa.Column('cited', sa.Boolean(), server_default='false', nullable=False), - sa.Column('citation_position', sa.Integer(), nullable=True), - sa.Column('citation_text', sa.Text(), nullable=True), - sa.Column('competitor_brands', postgresql.JSONB(), server_default='[]', nullable=False), - sa.Column('raw_response', sa.Text(), nullable=True), - sa.Column('queried_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ondelete='CASCADE') - ) - op.create_index('idx_citation_records_query_id', 'citation_records', ['query_id']) - op.create_index('idx_citation_records_queried_at', 'citation_records', ['queried_at']) - op.create_index('idx_citation_records_platform', 'citation_records', ['platform']) - - # query_tasks table - op.create_table( - 'query_tasks', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('query_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('platform', sa.String(50), nullable=False), - sa.Column('status', sa.String(20), server_default='pending', nullable=False), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('scheduled_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ondelete='CASCADE') - ) - op.create_index('idx_query_tasks_status', 'query_tasks', ['status']) - - # subscriptions table - op.create_table( - 'subscriptions', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('plan', sa.String(20), nullable=False), - sa.Column('status', sa.String(20), server_default='active', nullable=False), - sa.Column('start_date', sa.Date(), nullable=False), - sa.Column('end_date', sa.Date(), nullable=False), - sa.Column('amount', sa.Numeric(10, 2), nullable=True), - sa.Column('payment_method', sa.String(50), nullable=True), - sa.Column('payment_id', sa.String(255), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE') - ) - - -def downgrade() -> None: - """Downgrade schema.""" - op.drop_table('subscriptions') - op.drop_index('idx_query_tasks_status', table_name='query_tasks') - op.drop_table('query_tasks') - op.drop_index('idx_citation_records_platform', table_name='citation_records') - op.drop_index('idx_citation_records_queried_at', table_name='citation_records') - op.drop_index('idx_citation_records_query_id', table_name='citation_records') - op.drop_table('citation_records') - op.drop_index('idx_queries_next_query_at', table_name='queries') - op.drop_index('idx_queries_status', table_name='queries') - op.drop_index('idx_queries_user_id', table_name='queries') - op.drop_table('queries') - op.drop_table('users') diff --git a/backend/alembic/versions/810a29804f5a_sync_users_nullable_state.py b/backend/alembic/versions/810a29804f5a_sync_users_nullable_state.py deleted file mode 100644 index 7a74540..0000000 --- a/backend/alembic/versions/810a29804f5a_sync_users_nullable_state.py +++ /dev/null @@ -1,48 +0,0 @@ -"""sync_users_nullable_state - -Revision ID: 810a29804f5a -Revises: e5f7a9b1cd35 -Create Date: 2026-05-23 21:45:35.491924 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '810a29804f5a' -down_revision: Union[str, Sequence[str], None] = 'e5f7a9b1cd35' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('users', 'email_verified', - existing_type=sa.BOOLEAN(), - nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('users', 'is_admin', - existing_type=sa.BOOLEAN(), - nullable=False, - existing_server_default=sa.text('false')) - op.drop_index(op.f('idx_users_organization_id'), table_name='users') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_index(op.f('idx_users_organization_id'), 'users', ['organization_id'], unique=False) - op.alter_column('users', 'is_admin', - existing_type=sa.BOOLEAN(), - nullable=True, - existing_server_default=sa.text('false')) - op.alter_column('users', 'email_verified', - existing_type=sa.BOOLEAN(), - nullable=True, - existing_server_default=sa.text('false')) - # ### end Alembic commands ### diff --git a/backend/alembic/versions/8ccb553ff975_add_citation_source_analysis_fields.py b/backend/alembic/versions/8ccb553ff975_add_citation_source_analysis_fields.py deleted file mode 100644 index 12a3255..0000000 --- a/backend/alembic/versions/8ccb553ff975_add_citation_source_analysis_fields.py +++ /dev/null @@ -1,61 +0,0 @@ -"""add_citation_source_analysis_fields - -Revision ID: 8ccb553ff975 -Revises: 059724556401 -Create Date: 2026-05-23 17:23:03.183460 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '8ccb553ff975' -down_revision: Union[str, Sequence[str], None] = '059724556401' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # 数据来源类型标记 - op.add_column( - 'citation_records', - sa.Column('data_source', sa.String(20), nullable=True, - comment='数据来源类型: ai_platform / search_engine / unknown') - ) - # 提取的引用URL列表 - op.add_column( - 'citation_records', - sa.Column('source_urls', sa.JSON(), nullable=True, - comment='提取的引用URL列表') - ) - # 提取的引用来源标题列表 - op.add_column( - 'citation_records', - sa.Column('source_titles', sa.JSON(), nullable=True, - comment='提取的引用来源标题列表') - ) - # 引用出现的上下文片段列表 - op.add_column( - 'citation_records', - sa.Column('citation_contexts', sa.JSON(), nullable=True, - comment='引用出现的上下文片段列表') - ) - # AI回答原始文本 - op.add_column( - 'citation_records', - sa.Column('ai_response_text', sa.Text(), nullable=True, - comment='AI回答原始文本(去掉data_source标记后的纯文本)') - ) - - -def downgrade() -> None: - """Downgrade schema.""" - op.drop_column('citation_records', 'ai_response_text') - op.drop_column('citation_records', 'citation_contexts') - op.drop_column('citation_records', 'source_titles') - op.drop_column('citation_records', 'source_urls') - op.drop_column('citation_records', 'data_source') diff --git a/backend/alembic/versions/a79329c23b20_initial_complete_schema.py b/backend/alembic/versions/a79329c23b20_initial_complete_schema.py new file mode 100644 index 0000000..5d827e4 --- /dev/null +++ b/backend/alembic/versions/a79329c23b20_initial_complete_schema.py @@ -0,0 +1,1099 @@ +"""initial complete schema + +Revision ID: a79329c23b20 +Revises: +Create Date: 2026-06-01 22:01:36.025373 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a79329c23b20' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('agent_registry', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('display_name', sa.String(length=100), nullable=True), + sa.Column('agent_type', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('version', sa.String(length=20), nullable=True), + sa.Column('endpoint', sa.String(length=500), nullable=True), + sa.Column('status', sa.String(length=20), server_default='offline', nullable=False), + sa.Column('capabilities', sa.JSON(), nullable=True), + sa.Column('last_heartbeat', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_index('idx_agent_registry_agent_type', 'agent_registry', ['agent_type'], unique=False) + op.create_index('idx_agent_registry_name', 'agent_registry', ['name'], unique=False) + op.create_index('idx_agent_registry_status', 'agent_registry', ['status'], unique=False) + op.create_table('api_keys', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('engine_type', sa.String(length=20), nullable=False), + sa.Column('encrypted_key', sa.String(length=500), nullable=False), + sa.Column('key_hint', sa.String(length=50), nullable=False), + sa.Column('key_source', sa.String(length=10), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('priority', sa.Integer(), nullable=False), + sa.Column('last_verified_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_api_keys_engine_status', 'api_keys', ['engine_type', 'status'], unique=False) + op.create_index('idx_api_keys_user_engine', 'api_keys', ['user_id', 'engine_type'], unique=False) + op.create_index(op.f('ix_api_keys_user_id'), 'api_keys', ['user_id'], unique=False) + op.create_table('brands', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('aliases', sa.JSON(), nullable=False), + sa.Column('website', sa.String(length=500), nullable=True), + sa.Column('industry', sa.String(length=50), nullable=True), + sa.Column('platforms', sa.JSON(), nullable=False), + sa.Column('frequency', sa.String(length=20), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('last_queried_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_query_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('optimization_insights', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('organization_id', sa.String(length=36), nullable=False), + sa.Column('content_id', sa.String(length=36), nullable=True), + sa.Column('insight_type', sa.String(length=30), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('recommendation', sa.Text(), nullable=False), + sa.Column('severity', sa.String(length=20), nullable=False), + sa.Column('applied', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_optimization_insights_organization_id'), 'optimization_insights', ['organization_id'], unique=False) + op.create_table('organizations', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('slug', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('logo_url', sa.String(length=500), nullable=True), + sa.Column('plan', sa.String(length=20), server_default='free', nullable=False), + sa.Column('max_members', sa.Integer(), server_default='5', nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('slug') + ) + op.create_index('idx_organizations_slug', 'organizations', ['slug'], unique=False) + op.create_table('platform_rule_versions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('rule_id', sa.String(length=100), nullable=False), + sa.Column('platform', sa.String(length=50), nullable=False), + sa.Column('version', sa.Integer(), nullable=False), + sa.Column('rule_data', sa.JSON(), nullable=False), + sa.Column('change_summary', sa.String(length=500), nullable=True), + sa.Column('created_by', sa.String(length=100), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_rule_versions_platform', 'platform_rule_versions', ['platform'], unique=False) + op.create_index('idx_rule_versions_rule_id', 'platform_rule_versions', ['rule_id'], unique=False) + op.create_index(op.f('ix_platform_rule_versions_rule_id'), 'platform_rule_versions', ['rule_id'], unique=False) + op.create_table('platform_rules', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('platform', sa.String(length=50), nullable=False), + sa.Column('rule_category', sa.String(length=50), nullable=False), + sa.Column('rule_name', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('check_criteria', sa.JSON(), nullable=True), + sa.Column('severity', sa.String(length=20), nullable=False), + sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_platform_rules_is_active', 'platform_rules', ['is_active'], unique=False) + op.create_index('idx_platform_rules_platform', 'platform_rules', ['platform'], unique=False) + op.create_index('idx_platform_rules_platform_category', 'platform_rules', ['platform', 'rule_category'], unique=False) + op.create_index('idx_platform_rules_rule_category', 'platform_rules', ['rule_category'], unique=False) + op.create_table('publish_records', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('organization_id', sa.String(length=36), nullable=False), + sa.Column('content_title', sa.String(length=200), nullable=False), + sa.Column('content_id', sa.String(length=36), nullable=True), + sa.Column('platform', sa.String(length=50), nullable=False), + sa.Column('published_url', sa.String(length=500), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('published_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_publish_records_organization_id'), 'publish_records', ['organization_id'], unique=False) + op.create_table('alert_settings', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('alert_type', sa.String(length=50), nullable=False, comment='告警类型: score_drop / score_rise / negative_sentiment / competitor_overtake / new_platform_mention'), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('threshold', sa.Float(), nullable=True, comment='阈值(如评分下降超过5分触发)'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_alert_settings_brand_id', 'alert_settings', ['brand_id'], unique=False) + op.create_index('idx_alert_settings_brand_type', 'alert_settings', ['brand_id', 'alert_type'], unique=True) + op.create_index('idx_alert_settings_user_id', 'alert_settings', ['user_id'], unique=False) + op.create_index(op.f('ix_alert_settings_user_id'), 'alert_settings', ['user_id'], unique=False) + op.create_table('alerts', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('alert_type', sa.String(length=50), nullable=False, comment='告警类型: score_drop / score_rise / negative_sentiment / competitor_overtake / new_platform_mention'), + sa.Column('severity', sa.String(length=20), nullable=False, comment='严重程度: critical / warning / info'), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('message', sa.Text(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True, comment='告警相关数据(JSON)'), + sa.Column('is_read', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_alerts_alert_type', 'alerts', ['alert_type'], unique=False) + op.create_index('idx_alerts_brand_id', 'alerts', ['brand_id'], unique=False) + op.create_index('idx_alerts_created_at', 'alerts', ['created_at'], unique=False) + op.create_index('idx_alerts_is_read', 'alerts', ['is_read'], unique=False) + op.create_index('idx_alerts_user_id', 'alerts', ['user_id'], unique=False) + op.create_index('idx_alerts_user_read', 'alerts', ['user_id', 'is_read'], unique=False) + op.create_index(op.f('ix_alerts_user_id'), 'alerts', ['user_id'], unique=False) + op.create_table('competitor_insights', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('competitor_name', sa.String(length=100), nullable=False), + sa.Column('analysis_type', sa.String(length=50), nullable=False), + sa.Column('insight_data', sa.JSON(), nullable=True), + sa.Column('citation_count_brand', sa.Integer(), nullable=False), + sa.Column('citation_count_competitor', sa.Integer(), nullable=False), + sa.Column('sentiment_brand', sa.Float(), nullable=True), + sa.Column('sentiment_competitor', sa.Float(), nullable=True), + sa.Column('platform_breakdown', sa.JSON(), nullable=True), + sa.Column('gap_analysis', sa.JSON(), nullable=True), + sa.Column('opportunity_areas', sa.JSON(), nullable=True), + sa.Column('recommendations', sa.JSON(), nullable=True), + sa.Column('confidence', sa.String(length=20), nullable=False), + sa.Column('period_days', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_competitor_insights_analysis_type', 'competitor_insights', ['analysis_type'], unique=False) + op.create_index('idx_competitor_insights_brand_id', 'competitor_insights', ['brand_id'], unique=False) + op.create_table('competitors', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('aliases', sa.JSON(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('content_metrics', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('publish_record_id', sa.String(length=36), nullable=False), + sa.Column('recorded_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('views', sa.Integer(), nullable=False), + sa.Column('likes', sa.Integer(), nullable=False), + sa.Column('comments', sa.Integer(), nullable=False), + sa.Column('shares', sa.Integer(), nullable=False), + sa.Column('bookmarks', sa.Integer(), nullable=False), + sa.Column('ai_citation_count', sa.Integer(), nullable=False), + sa.Column('search_impressions', sa.Integer(), nullable=False), + sa.Column('search_clicks', sa.Integer(), nullable=False), + sa.Column('avg_read_duration', sa.Float(), nullable=False), + sa.Column('read_completion_rate', sa.Float(), nullable=False), + sa.ForeignKeyConstraint(['publish_record_id'], ['publish_records.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('detection_tasks', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('frequency', sa.String(length=20), nullable=False), + sa.Column('engines', sa.JSON(), nullable=False), + sa.Column('queries', sa.JSON(), nullable=False), + sa.Column('competitor_names', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('last_run_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_run_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_detection_tasks_brand_id', 'detection_tasks', ['brand_id'], unique=False) + op.create_index('idx_detection_tasks_is_active', 'detection_tasks', ['is_active'], unique=False) + op.create_index('idx_detection_tasks_user_id', 'detection_tasks', ['user_id'], unique=False) + op.create_index(op.f('ix_detection_tasks_user_id'), 'detection_tasks', ['user_id'], unique=False) + op.create_table('diagnosis_records', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('diagnosis_type', sa.String(length=20), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('overall_score', sa.Float(), nullable=True), + sa.Column('result_json', sa.JSON(), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('collection_metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_diagnosis_records_brand_id', 'diagnosis_records', ['brand_id'], unique=False) + op.create_index('idx_diagnosis_records_created_at', 'diagnosis_records', ['created_at'], unique=False) + op.create_index('idx_diagnosis_records_status', 'diagnosis_records', ['status'], unique=False) + op.create_index('idx_diagnosis_records_user_id', 'diagnosis_records', ['user_id'], unique=False) + op.create_table('monitoring_records', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('content_id', sa.String(length=36), nullable=True), + sa.Column('query_keywords', sa.String(length=500), nullable=True), + sa.Column('platform', sa.String(length=50), nullable=True), + sa.Column('baseline_citation_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('baseline_sentiment', sa.Float(), nullable=True), + sa.Column('baseline_rank', sa.Integer(), nullable=True), + sa.Column('current_citation_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('current_sentiment', sa.Float(), nullable=True), + sa.Column('current_rank', sa.Integer(), nullable=True), + sa.Column('change_type', sa.String(length=20), nullable=True), + sa.Column('change_details', sa.JSON(), nullable=True), + sa.Column('check_interval_hours', sa.Integer(), server_default='24', nullable=False), + sa.Column('last_checked_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_check_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('status', sa.String(length=20), server_default='active', nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_monitoring_records_brand_id', 'monitoring_records', ['brand_id'], unique=False) + op.create_index('idx_monitoring_records_next_check_at', 'monitoring_records', ['next_check_at'], unique=False) + op.create_index('idx_monitoring_records_status', 'monitoring_records', ['status'], unique=False) + op.create_table('schema_suggestions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('schema_type', sa.String(length=50), nullable=False), + sa.Column('target_url', sa.String(length=500), nullable=True), + sa.Column('json_ld_template', sa.JSON(), nullable=False), + sa.Column('json_ld_filled', sa.JSON(), nullable=True), + sa.Column('priority', sa.String(length=20), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('diagnosis_dimensions', sa.JSON(), nullable=True), + sa.Column('implementation_difficulty', sa.String(length=20), nullable=False), + sa.Column('estimated_impact', sa.Text(), nullable=True), + sa.Column('validation_errors', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_schema_suggestions_brand_id', 'schema_suggestions', ['brand_id'], unique=False) + op.create_index('idx_schema_suggestions_brand_status', 'schema_suggestions', ['brand_id', 'status'], unique=False) + op.create_index('idx_schema_suggestions_schema_type', 'schema_suggestions', ['schema_type'], unique=False) + op.create_index('idx_schema_suggestions_status', 'schema_suggestions', ['status'], unique=False) + op.create_table('suggestions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('type', sa.String(length=50), nullable=False, comment='建议类型: content_optimization/platform_targeting/competitor_gap/query_expansion/citation_improvement'), + sa.Column('priority', sa.String(length=20), nullable=False, comment='优先级: high/medium/low'), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('action', sa.Text(), nullable=True, comment='具体操作步骤'), + sa.Column('expected_impact', sa.String(length=200), nullable=True, comment='预期效果'), + sa.Column('difficulty', sa.String(length=20), nullable=False, comment='难度: easy/medium/hard'), + sa.Column('status', sa.String(length=20), nullable=False, comment='状态: pending/in_progress/completed/dismissed'), + sa.Column('generated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('batch_id', sa.UUID(), nullable=False), + sa.Column('source', sa.String(length=20), nullable=False, comment='生成来源: rule/llm'), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_suggestions_batch_id', 'suggestions', ['batch_id'], unique=False) + op.create_index('idx_suggestions_brand_id', 'suggestions', ['brand_id'], unique=False) + op.create_index('idx_suggestions_brand_status', 'suggestions', ['brand_id', 'status'], unique=False) + op.create_index('idx_suggestions_status', 'suggestions', ['status'], unique=False) + op.create_index('idx_suggestions_type', 'suggestions', ['type'], unique=False) + op.create_table('trend_insights', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('trend_type', sa.String(length=20), nullable=False), + sa.Column('keyword', sa.String(length=200), nullable=True), + sa.Column('platform', sa.String(length=50), nullable=True), + sa.Column('period_start', sa.DateTime(timezone=True), nullable=False), + sa.Column('period_end', sa.DateTime(timezone=True), nullable=False), + sa.Column('data_points', sa.JSON(), nullable=True), + sa.Column('change_rate', sa.Float(), nullable=True), + sa.Column('absolute_change', sa.Integer(), nullable=True), + sa.Column('sentiment_trend', sa.JSON(), nullable=True), + sa.Column('cause_analysis', sa.Text(), nullable=True), + sa.Column('recommendations', sa.JSON(), nullable=True), + sa.Column('confidence', sa.Float(), nullable=False), + sa.Column('severity', sa.String(length=20), server_default='info', nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_trend_insights_brand_id', 'trend_insights', ['brand_id'], unique=False) + op.create_index('idx_trend_insights_created_at', 'trend_insights', ['created_at'], unique=False) + op.create_index('idx_trend_insights_period_start', 'trend_insights', ['period_start'], unique=False) + op.create_index('idx_trend_insights_trend_type', 'trend_insights', ['trend_type'], unique=False) + op.create_table('usage_records', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=True), + sa.Column('engine_type', sa.String(length=20), nullable=False), + sa.Column('query', sa.String(length=500), nullable=False), + sa.Column('input_tokens', sa.Integer(), nullable=False), + sa.Column('output_tokens', sa.Integer(), nullable=False), + sa.Column('cost', sa.Float(), nullable=False), + sa.Column('extra_data', sa.JSON(), nullable=False), + sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_usage_records_engine_timestamp', 'usage_records', ['engine_type', 'timestamp'], unique=False) + op.create_index('idx_usage_records_user_engine', 'usage_records', ['user_id', 'engine_type'], unique=False) + op.create_index('idx_usage_records_user_timestamp', 'usage_records', ['user_id', 'timestamp'], unique=False) + op.create_index(op.f('ix_usage_records_timestamp'), 'usage_records', ['timestamp'], unique=False) + op.create_index(op.f('ix_usage_records_user_id'), 'usage_records', ['user_id'], unique=False) + op.create_table('users', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('email', sa.Text(), nullable=False), + sa.Column('phone', sa.Text(), nullable=True), + sa.Column('username', sa.Text(), nullable=True), + sa.Column('password', sa.Text(), nullable=False), + sa.Column('firstName', sa.Text(), nullable=True), + sa.Column('lastName', sa.Text(), nullable=True), + sa.Column('avatar', sa.Text(), nullable=True), + sa.Column('isActive', sa.Boolean(), nullable=False), + sa.Column('emailVerified', sa.Boolean(), nullable=False), + sa.Column('phoneVerified', sa.Boolean(), nullable=False), + sa.Column('lastLoginAt', sa.DateTime(timezone=True), nullable=True), + sa.Column('createdAt', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updatedAt', sa.DateTime(timezone=True), nullable=False), + sa.Column('mfaSecret', sa.Text(), nullable=True), + sa.Column('mfaEnabled', sa.Boolean(), nullable=False), + sa.Column('loginAttempts', sa.Integer(), nullable=False), + sa.Column('lockedUntil', sa.DateTime(timezone=True), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=True), + sa.Column('role', sa.String(length=20), server_default='owner', nullable=False), + sa.Column('plan', sa.String(length=20), server_default='free', nullable=False), + sa.Column('max_queries', sa.Integer(), server_default='5', nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + op.create_table('agent_configs', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=False), + sa.Column('config_key', sa.String(length=100), nullable=False), + sa.Column('config_value', sa.JSON(), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_by', sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agent_registry.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['updated_by'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_agent_configs_agent_id', 'agent_configs', ['agent_id'], unique=False) + op.create_index('idx_agent_configs_agent_key', 'agent_configs', ['agent_id', 'config_key'], unique=True) + op.create_table('brand_knowledge', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('category', sa.String(length=50), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False), + sa.Column('created_by', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_brand_knowledge_category', 'brand_knowledge', ['category'], unique=False) + op.create_index('idx_brand_knowledge_is_active', 'brand_knowledge', ['is_active'], unique=False) + op.create_index('idx_brand_knowledge_organization_id', 'brand_knowledge', ['organization_id'], unique=False) + op.create_table('content_baselines', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('monitoring_record_id', sa.UUID(), nullable=False), + sa.Column('brand_name', sa.String(length=100), nullable=False), + sa.Column('keyword', sa.String(length=200), nullable=False), + sa.Column('platform', sa.String(length=50), nullable=False), + sa.Column('citation_count', sa.Integer(), nullable=False), + sa.Column('sentiment_score', sa.Float(), nullable=True), + sa.Column('rank_position', sa.Integer(), nullable=True), + sa.Column('snapshot_data', sa.JSON(), nullable=True), + sa.Column('recorded_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['monitoring_record_id'], ['monitoring_records.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_content_baselines_monitoring_record_id', 'content_baselines', ['monitoring_record_id'], unique=False) + op.create_table('geo_plans', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('title', sa.String(length=500), nullable=False), + sa.Column('status', sa.String(length=20), server_default='draft', nullable=False), + sa.Column('diagnosis_score', sa.Integer(), nullable=False), + sa.Column('target_score', sa.Integer(), nullable=False), + sa.Column('estimated_weeks', sa.Integer(), nullable=False), + sa.Column('plan_data', sa.JSON(), nullable=True), + sa.Column('source', sa.String(length=20), nullable=False), + sa.Column('created_by', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_geo_plans_brand_id', 'geo_plans', ['brand_id'], unique=False) + op.create_index('idx_geo_plans_organization_id', 'geo_plans', ['organization_id'], unique=False) + op.create_index('idx_geo_plans_status', 'geo_plans', ['status'], unique=False) + op.create_table('knowledge_bases', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('type', sa.String(length=20), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('document_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('status', sa.String(length=20), server_default='active', nullable=False), + sa.Column('created_by', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_knowledge_bases_organization_id', 'knowledge_bases', ['organization_id'], unique=False) + op.create_index('idx_knowledge_bases_status', 'knowledge_bases', ['status'], unique=False) + op.create_index('idx_knowledge_bases_type', 'knowledge_bases', ['type'], unique=False) + op.create_table('knowledge_search_logs', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.String(length=36), nullable=True), + sa.Column('query', sa.Text(), nullable=False), + sa.Column('knowledge_base_ids', sa.JSON(), nullable=True), + sa.Column('results_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('latency_ms', sa.Integer(), server_default='0', nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_knowledge_search_logs_created_at', 'knowledge_search_logs', ['created_at'], unique=False) + op.create_index('idx_knowledge_search_logs_organization_id', 'knowledge_search_logs', ['organization_id'], unique=False) + op.create_index('idx_knowledge_search_logs_user_id', 'knowledge_search_logs', ['user_id'], unique=False) + op.create_table('lifecycle_projects', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('brand_name', sa.String(length=100), nullable=False), + sa.Column('brand_aliases', sa.JSON(), server_default='[]', nullable=False), + sa.Column('current_stage', sa.Integer(), server_default='1', nullable=False), + sa.Column('status', sa.String(length=20), server_default='active', nullable=False), + sa.Column('created_by', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_lifecycle_projects_brand_name', 'lifecycle_projects', ['brand_name'], unique=False) + op.create_index('idx_lifecycle_projects_organization_id', 'lifecycle_projects', ['organization_id'], unique=False) + op.create_index('idx_lifecycle_projects_status', 'lifecycle_projects', ['status'], unique=False) + op.create_table('org_members', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.Column('role', sa.String(length=20), server_default='viewer', nullable=False), + sa.Column('joined_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('invited_by', sa.UUID(), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_org_members_org_user', 'org_members', ['organization_id', 'user_id'], unique=True) + op.create_index('idx_org_members_organization_id', 'org_members', ['organization_id'], unique=False) + op.create_index('idx_org_members_user_id', 'org_members', ['user_id'], unique=False) + op.create_table('payment_orders', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.Column('plan', sa.String(length=20), nullable=False), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('currency', sa.String(length=10), nullable=False), + sa.Column('payment_provider', sa.String(length=20), nullable=False), + sa.Column('payment_id', sa.String(length=255), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('pay_url', sa.String(length=1024), nullable=True), + sa.Column('callback_data', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('paid_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('queries', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.Column('keyword', sa.String(length=200), nullable=False), + sa.Column('target_brand', sa.String(length=100), nullable=False), + sa.Column('brand_aliases', sa.JSON(), nullable=False), + sa.Column('platforms', sa.JSON(), nullable=False), + sa.Column('frequency', sa.String(length=20), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('last_queried_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_query_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_queries_next_query_at', 'queries', ['next_query_at'], unique=False) + op.create_index('idx_queries_status', 'queries', ['status'], unique=False) + op.create_index('idx_queries_user_id', 'queries', ['user_id'], unique=False) + op.create_table('subscriptions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.Column('plan', sa.String(length=20), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('start_date', sa.Date(), nullable=False), + sa.Column('end_date', sa.Date(), nullable=False), + sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('payment_method', sa.String(length=50), nullable=True), + sa.Column('payment_id', sa.String(length=255), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('agent_tasks', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=False), + sa.Column('task_type', sa.String(length=50), nullable=False), + sa.Column('status', sa.String(length=20), server_default='pending', nullable=False), + sa.Column('priority', sa.Integer(), server_default='0', nullable=False), + sa.Column('input_data', sa.JSON(), nullable=True), + sa.Column('output_data', sa.JSON(), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('created_by', sa.String(length=36), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('project_id', sa.UUID(), nullable=True), + sa.Column('scheduled_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['agent_id'], ['agent_registry.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['project_id'], ['lifecycle_projects.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_agent_tasks_agent_id', 'agent_tasks', ['agent_id'], unique=False) + op.create_index('idx_agent_tasks_created_by', 'agent_tasks', ['created_by'], unique=False) + op.create_index('idx_agent_tasks_organization_id', 'agent_tasks', ['organization_id'], unique=False) + op.create_index('idx_agent_tasks_project_id', 'agent_tasks', ['project_id'], unique=False) + op.create_index('idx_agent_tasks_status', 'agent_tasks', ['status'], unique=False) + op.create_index('idx_agent_tasks_task_type', 'agent_tasks', ['task_type'], unique=False) + op.create_table('citation_records', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('query_id', sa.UUID(), nullable=False), + sa.Column('platform', sa.String(length=50), nullable=False), + sa.Column('cited', sa.Boolean(), nullable=False), + sa.Column('citation_position', sa.Integer(), nullable=True), + sa.Column('citation_text', sa.Text(), nullable=True), + sa.Column('competitor_brands', sa.JSON(), nullable=False), + sa.Column('raw_response', sa.Text(), nullable=True), + sa.Column('confidence', sa.Float(), nullable=True), + sa.Column('match_type', sa.String(length=20), nullable=True), + sa.Column('sentiment', sa.String(length=20), nullable=True, comment='情感倾向: positive / neutral / negative'), + sa.Column('sentiment_confidence', sa.Float(), nullable=True, comment='情感分析置信度 0.0-1.0'), + sa.Column('sentiment_key_phrases', sa.JSON(), nullable=True, comment='关键情感短语列表'), + sa.Column('data_source', sa.String(length=20), nullable=True, comment='数据来源类型: ai_platform / search_engine / unknown'), + sa.Column('source_urls', sa.JSON(), nullable=True, comment='提取的引用URL列表'), + sa.Column('source_titles', sa.JSON(), nullable=True, comment='提取的引用来源标题列表'), + sa.Column('citation_contexts', sa.JSON(), nullable=True, comment='引用出现的上下文片段列表'), + sa.Column('ai_response_text', sa.Text(), nullable=True, comment='AI回答原始文本(去掉data_source标记后的纯文本)'), + sa.Column('queried_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_citation_records_platform', 'citation_records', ['platform'], unique=False) + op.create_index('idx_citation_records_queried_at', 'citation_records', ['queried_at'], unique=False) + op.create_index('idx_citation_records_query_id', 'citation_records', ['query_id'], unique=False) + op.create_table('contents', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('project_id', sa.UUID(), nullable=True), + sa.Column('title', sa.String(length=500), nullable=False), + sa.Column('content_type', sa.String(length=50), nullable=False), + sa.Column('body', sa.Text(), nullable=True), + sa.Column('status', sa.String(length=20), server_default='draft', nullable=False), + sa.Column('target_platforms', sa.JSON(), nullable=True), + sa.Column('keywords', sa.JSON(), nullable=True), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('created_by', sa.String(length=36), nullable=True), + sa.Column('current_version', sa.Integer(), server_default='1', nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['project_id'], ['lifecycle_projects.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_contents_content_type', 'contents', ['content_type'], unique=False) + op.create_index('idx_contents_created_by', 'contents', ['created_by'], unique=False) + op.create_index('idx_contents_organization_id', 'contents', ['organization_id'], unique=False) + op.create_index('idx_contents_project_id', 'contents', ['project_id'], unique=False) + op.create_index('idx_contents_status', 'contents', ['status'], unique=False) + op.create_table('geo_plan_actions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('plan_id', sa.UUID(), nullable=False), + sa.Column('action_type', sa.String(length=50), nullable=False), + sa.Column('title', sa.String(length=500), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('reason', sa.Text(), nullable=False), + sa.Column('priority', sa.String(length=10), nullable=False), + sa.Column('status', sa.String(length=20), server_default='pending', nullable=False), + sa.Column('target_keyword', sa.String(length=200), nullable=True), + sa.Column('target_platform', sa.String(length=50), nullable=True), + sa.Column('content_style', sa.String(length=50), nullable=True), + sa.Column('estimated_impact', sa.String(length=500), nullable=True), + sa.Column('difficulty', sa.String(length=10), nullable=False), + sa.Column('execution_params', sa.JSON(), nullable=True), + sa.Column('sort_order', sa.Integer(), server_default='0', nullable=False), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['plan_id'], ['geo_plans.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_geo_plan_actions_plan_id', 'geo_plan_actions', ['plan_id'], unique=False) + op.create_index('idx_geo_plan_actions_priority', 'geo_plan_actions', ['priority'], unique=False) + op.create_index('idx_geo_plan_actions_status', 'geo_plan_actions', ['status'], unique=False) + op.create_table('keywords', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('project_id', sa.UUID(), nullable=True), + sa.Column('keyword', sa.String(length=200), nullable=False), + sa.Column('category', sa.String(length=50), nullable=False), + sa.Column('priority', sa.Integer(), server_default='0', nullable=False), + sa.Column('search_volume', sa.Integer(), nullable=True), + sa.Column('competition_level', sa.String(length=20), nullable=True), + sa.Column('status', sa.String(length=20), server_default='active', nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['project_id'], ['lifecycle_projects.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_keywords_category', 'keywords', ['category'], unique=False) + op.create_index('idx_keywords_keyword', 'keywords', ['keyword'], unique=False) + op.create_index('idx_keywords_organization_id', 'keywords', ['organization_id'], unique=False) + op.create_index('idx_keywords_project_id', 'keywords', ['project_id'], unique=False) + op.create_index('idx_keywords_status', 'keywords', ['status'], unique=False) + op.create_table('knowledge_documents', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('knowledge_base_id', sa.UUID(), nullable=False), + sa.Column('title', sa.String(length=500), nullable=False), + sa.Column('source_type', sa.String(length=20), nullable=False), + sa.Column('source_url', sa.String(length=2000), nullable=True), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('content_hash', sa.String(length=64), nullable=False), + sa.Column('chunk_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('status', sa.String(length=20), server_default='processing', nullable=False), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['knowledge_base_id'], ['knowledge_bases.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_knowledge_documents_content_hash', 'knowledge_documents', ['content_hash'], unique=False) + op.create_index('idx_knowledge_documents_knowledge_base_id', 'knowledge_documents', ['knowledge_base_id'], unique=False) + op.create_index('idx_knowledge_documents_status', 'knowledge_documents', ['status'], unique=False) + op.create_table('project_stages', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('project_id', sa.UUID(), nullable=False), + sa.Column('stage_number', sa.Integer(), nullable=False), + sa.Column('status', sa.String(length=20), server_default='pending', nullable=False), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('metrics', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['lifecycle_projects.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_project_stages_project_id', 'project_stages', ['project_id'], unique=False) + op.create_index('idx_project_stages_project_stage', 'project_stages', ['project_id', 'stage_number'], unique=True) + op.create_index('idx_project_stages_status', 'project_stages', ['status'], unique=False) + op.create_table('query_tasks', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('query_id', sa.UUID(), nullable=False), + sa.Column('platform', sa.String(length=50), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('scheduled_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_query_tasks_status', 'query_tasks', ['status'], unique=False) + op.create_table('agent_task_logs', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('task_id', sa.UUID(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=False), + sa.Column('log_level', sa.String(length=10), nullable=False), + sa.Column('message', sa.Text(), nullable=False), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['agent_id'], ['agent_registry.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['task_id'], ['agent_tasks.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_agent_task_logs_agent_id', 'agent_task_logs', ['agent_id'], unique=False) + op.create_index('idx_agent_task_logs_created_at', 'agent_task_logs', ['created_at'], unique=False) + op.create_index('idx_agent_task_logs_task_id', 'agent_task_logs', ['task_id'], unique=False) + op.create_table('attribution_records', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('brand_id', sa.UUID(), nullable=False), + sa.Column('content_id', sa.UUID(), nullable=True), + sa.Column('baseline_score', sa.Float(), nullable=False), + sa.Column('current_score', sa.Float(), nullable=True), + sa.Column('score_delta', sa.Float(), nullable=True), + sa.Column('attribution_window_days', sa.Integer(), server_default='28', nullable=False), + sa.Column('published_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('window_end_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('status', sa.String(length=20), server_default='tracking', nullable=False), + sa.Column('attributed_dimensions', sa.JSON(), nullable=True), + sa.Column('roi_percentage', sa.Float(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['brand_id'], ['brands.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['content_id'], ['contents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_attribution_records_brand_id', 'attribution_records', ['brand_id'], unique=False) + op.create_index('idx_attribution_records_content_id', 'attribution_records', ['content_id'], unique=False) + op.create_index('idx_attribution_records_status', 'attribution_records', ['status'], unique=False) + op.create_index('idx_attribution_records_user_id', 'attribution_records', ['user_id'], unique=False) + op.create_table('content_reviews', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('content_id', sa.UUID(), nullable=False), + sa.Column('reviewer_id', sa.String(length=36), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('comments', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['content_id'], ['contents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['reviewer_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_content_reviews_content_id', 'content_reviews', ['content_id'], unique=False) + op.create_index('idx_content_reviews_reviewer_id', 'content_reviews', ['reviewer_id'], unique=False) + op.create_table('content_versions', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('content_id', sa.UUID(), nullable=False), + sa.Column('version_number', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=500), nullable=True), + sa.Column('body', sa.Text(), nullable=True), + sa.Column('change_summary', sa.String(length=500), nullable=True), + sa.Column('created_by', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['content_id'], ['contents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_content_versions_content_id', 'content_versions', ['content_id'], unique=False) + op.create_index('idx_content_versions_content_version', 'content_versions', ['content_id', 'version_number'], unique=True) + op.create_table('distribution_schedules', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('content_title', sa.String(length=500), nullable=False), + sa.Column('content_id', sa.UUID(), nullable=True), + sa.Column('platforms', sa.JSON(), nullable=True), + sa.Column('tips', sa.JSON(), nullable=True), + sa.Column('status', sa.String(length=20), server_default='pending', nullable=False), + sa.Column('created_by', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['content_id'], ['contents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_distribution_schedules_created_by', 'distribution_schedules', ['created_by'], unique=False) + op.create_index('idx_distribution_schedules_organization_id', 'distribution_schedules', ['organization_id'], unique=False) + op.create_index('idx_distribution_schedules_status', 'distribution_schedules', ['status'], unique=False) + op.create_table('knowledge_chunks', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('document_id', sa.UUID(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('embedding', sa.Text(), nullable=True), + sa.Column('chunk_index', sa.Integer(), nullable=False), + sa.Column('token_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['document_id'], ['knowledge_documents.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_knowledge_chunks_chunk_index', 'knowledge_chunks', ['document_id', 'chunk_index'], unique=False) + op.create_index('idx_knowledge_chunks_document_id', 'knowledge_chunks', ['document_id'], unique=False) + op.create_table('knowledge_entities', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('knowledge_base_id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=500), nullable=False), + sa.Column('entity_type', sa.Enum('ORGANIZATION', 'PRODUCT', 'PERSON', 'LOCATION', 'TECHNOLOGY', 'BRAND', 'EVENT', 'CONCEPT', 'OTHER', name='entitytype'), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('properties', sa.JSON(), nullable=True), + sa.Column('source_chunk_id', sa.UUID(), nullable=True), + sa.Column('confidence', sa.String(length=20), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['knowledge_base_id'], ['knowledge_bases.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['source_chunk_id'], ['knowledge_chunks.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_entities_kb_name', 'knowledge_entities', ['knowledge_base_id', 'name'], unique=False) + op.create_index('ix_entities_kb_type', 'knowledge_entities', ['knowledge_base_id', 'entity_type'], unique=False) + op.create_index(op.f('ix_knowledge_entities_entity_type'), 'knowledge_entities', ['entity_type'], unique=False) + op.create_index(op.f('ix_knowledge_entities_name'), 'knowledge_entities', ['name'], unique=False) + op.create_table('knowledge_relations', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('source_entity_id', sa.UUID(), nullable=False), + sa.Column('target_entity_id', sa.UUID(), nullable=False), + sa.Column('relation_type', sa.Enum('COMPETES_WITH', 'PARTNERS_WITH', 'ACQUIRES', 'SUBSIDIARY_OF', 'PRODUCES', 'USES_TECHNOLOGY', 'PART_OF', 'LOCATED_IN', 'FOUNDED_IN', 'CEO_OF', 'FOUNDER_OF', 'RELATED_TO', 'MENTIONED_IN', 'ALSO_KNOWN_AS', name='relationtype'), nullable=False), + sa.Column('properties', sa.JSON(), nullable=True), + sa.Column('source_chunk_id', sa.UUID(), nullable=True), + sa.Column('confidence', sa.String(length=20), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['source_chunk_id'], ['knowledge_chunks.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['source_entity_id'], ['knowledge_entities.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['target_entity_id'], ['knowledge_entities.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_knowledge_relations_relation_type'), 'knowledge_relations', ['relation_type'], unique=False) + op.create_index('ix_relations_source', 'knowledge_relations', ['source_entity_id'], unique=False) + op.create_index('ix_relations_target', 'knowledge_relations', ['target_entity_id'], unique=False) + op.create_index('ix_relations_type', 'knowledge_relations', ['relation_type'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('ix_relations_type', table_name='knowledge_relations') + op.drop_index('ix_relations_target', table_name='knowledge_relations') + op.drop_index('ix_relations_source', table_name='knowledge_relations') + op.drop_index(op.f('ix_knowledge_relations_relation_type'), table_name='knowledge_relations') + op.drop_table('knowledge_relations') + op.drop_index(op.f('ix_knowledge_entities_name'), table_name='knowledge_entities') + op.drop_index(op.f('ix_knowledge_entities_entity_type'), table_name='knowledge_entities') + op.drop_index('ix_entities_kb_type', table_name='knowledge_entities') + op.drop_index('ix_entities_kb_name', table_name='knowledge_entities') + op.drop_table('knowledge_entities') + op.drop_index('idx_knowledge_chunks_document_id', table_name='knowledge_chunks') + op.drop_index('idx_knowledge_chunks_chunk_index', table_name='knowledge_chunks') + op.drop_table('knowledge_chunks') + op.drop_index('idx_distribution_schedules_status', table_name='distribution_schedules') + op.drop_index('idx_distribution_schedules_organization_id', table_name='distribution_schedules') + op.drop_index('idx_distribution_schedules_created_by', table_name='distribution_schedules') + op.drop_table('distribution_schedules') + op.drop_index('idx_content_versions_content_version', table_name='content_versions') + op.drop_index('idx_content_versions_content_id', table_name='content_versions') + op.drop_table('content_versions') + op.drop_index('idx_content_reviews_reviewer_id', table_name='content_reviews') + op.drop_index('idx_content_reviews_content_id', table_name='content_reviews') + op.drop_table('content_reviews') + op.drop_index('idx_attribution_records_user_id', table_name='attribution_records') + op.drop_index('idx_attribution_records_status', table_name='attribution_records') + op.drop_index('idx_attribution_records_content_id', table_name='attribution_records') + op.drop_index('idx_attribution_records_brand_id', table_name='attribution_records') + op.drop_table('attribution_records') + op.drop_index('idx_agent_task_logs_task_id', table_name='agent_task_logs') + op.drop_index('idx_agent_task_logs_created_at', table_name='agent_task_logs') + op.drop_index('idx_agent_task_logs_agent_id', table_name='agent_task_logs') + op.drop_table('agent_task_logs') + op.drop_index('idx_query_tasks_status', table_name='query_tasks') + op.drop_table('query_tasks') + op.drop_index('idx_project_stages_status', table_name='project_stages') + op.drop_index('idx_project_stages_project_stage', table_name='project_stages') + op.drop_index('idx_project_stages_project_id', table_name='project_stages') + op.drop_table('project_stages') + op.drop_index('idx_knowledge_documents_status', table_name='knowledge_documents') + op.drop_index('idx_knowledge_documents_knowledge_base_id', table_name='knowledge_documents') + op.drop_index('idx_knowledge_documents_content_hash', table_name='knowledge_documents') + op.drop_table('knowledge_documents') + op.drop_index('idx_keywords_status', table_name='keywords') + op.drop_index('idx_keywords_project_id', table_name='keywords') + op.drop_index('idx_keywords_organization_id', table_name='keywords') + op.drop_index('idx_keywords_keyword', table_name='keywords') + op.drop_index('idx_keywords_category', table_name='keywords') + op.drop_table('keywords') + op.drop_index('idx_geo_plan_actions_status', table_name='geo_plan_actions') + op.drop_index('idx_geo_plan_actions_priority', table_name='geo_plan_actions') + op.drop_index('idx_geo_plan_actions_plan_id', table_name='geo_plan_actions') + op.drop_table('geo_plan_actions') + op.drop_index('idx_contents_status', table_name='contents') + op.drop_index('idx_contents_project_id', table_name='contents') + op.drop_index('idx_contents_organization_id', table_name='contents') + op.drop_index('idx_contents_created_by', table_name='contents') + op.drop_index('idx_contents_content_type', table_name='contents') + op.drop_table('contents') + op.drop_index('idx_citation_records_query_id', table_name='citation_records') + op.drop_index('idx_citation_records_queried_at', table_name='citation_records') + op.drop_index('idx_citation_records_platform', table_name='citation_records') + op.drop_table('citation_records') + op.drop_index('idx_agent_tasks_task_type', table_name='agent_tasks') + op.drop_index('idx_agent_tasks_status', table_name='agent_tasks') + op.drop_index('idx_agent_tasks_project_id', table_name='agent_tasks') + op.drop_index('idx_agent_tasks_organization_id', table_name='agent_tasks') + op.drop_index('idx_agent_tasks_created_by', table_name='agent_tasks') + op.drop_index('idx_agent_tasks_agent_id', table_name='agent_tasks') + op.drop_table('agent_tasks') + op.drop_table('subscriptions') + op.drop_index('idx_queries_user_id', table_name='queries') + op.drop_index('idx_queries_status', table_name='queries') + op.drop_index('idx_queries_next_query_at', table_name='queries') + op.drop_table('queries') + op.drop_table('payment_orders') + op.drop_index('idx_org_members_user_id', table_name='org_members') + op.drop_index('idx_org_members_organization_id', table_name='org_members') + op.drop_index('idx_org_members_org_user', table_name='org_members') + op.drop_table('org_members') + op.drop_index('idx_lifecycle_projects_status', table_name='lifecycle_projects') + op.drop_index('idx_lifecycle_projects_organization_id', table_name='lifecycle_projects') + op.drop_index('idx_lifecycle_projects_brand_name', table_name='lifecycle_projects') + op.drop_table('lifecycle_projects') + op.drop_index('idx_knowledge_search_logs_user_id', table_name='knowledge_search_logs') + op.drop_index('idx_knowledge_search_logs_organization_id', table_name='knowledge_search_logs') + op.drop_index('idx_knowledge_search_logs_created_at', table_name='knowledge_search_logs') + op.drop_table('knowledge_search_logs') + op.drop_index('idx_knowledge_bases_type', table_name='knowledge_bases') + op.drop_index('idx_knowledge_bases_status', table_name='knowledge_bases') + op.drop_index('idx_knowledge_bases_organization_id', table_name='knowledge_bases') + op.drop_table('knowledge_bases') + op.drop_index('idx_geo_plans_status', table_name='geo_plans') + op.drop_index('idx_geo_plans_organization_id', table_name='geo_plans') + op.drop_index('idx_geo_plans_brand_id', table_name='geo_plans') + op.drop_table('geo_plans') + op.drop_index('idx_content_baselines_monitoring_record_id', table_name='content_baselines') + op.drop_table('content_baselines') + op.drop_index('idx_brand_knowledge_organization_id', table_name='brand_knowledge') + op.drop_index('idx_brand_knowledge_is_active', table_name='brand_knowledge') + op.drop_index('idx_brand_knowledge_category', table_name='brand_knowledge') + op.drop_table('brand_knowledge') + op.drop_index('idx_agent_configs_agent_key', table_name='agent_configs') + op.drop_index('idx_agent_configs_agent_id', table_name='agent_configs') + op.drop_table('agent_configs') + op.drop_table('users') + op.drop_index(op.f('ix_usage_records_user_id'), table_name='usage_records') + op.drop_index(op.f('ix_usage_records_timestamp'), table_name='usage_records') + op.drop_index('idx_usage_records_user_timestamp', table_name='usage_records') + op.drop_index('idx_usage_records_user_engine', table_name='usage_records') + op.drop_index('idx_usage_records_engine_timestamp', table_name='usage_records') + op.drop_table('usage_records') + op.drop_index('idx_trend_insights_trend_type', table_name='trend_insights') + op.drop_index('idx_trend_insights_period_start', table_name='trend_insights') + op.drop_index('idx_trend_insights_created_at', table_name='trend_insights') + op.drop_index('idx_trend_insights_brand_id', table_name='trend_insights') + op.drop_table('trend_insights') + op.drop_index('idx_suggestions_type', table_name='suggestions') + op.drop_index('idx_suggestions_status', table_name='suggestions') + op.drop_index('idx_suggestions_brand_status', table_name='suggestions') + op.drop_index('idx_suggestions_brand_id', table_name='suggestions') + op.drop_index('idx_suggestions_batch_id', table_name='suggestions') + op.drop_table('suggestions') + op.drop_index('idx_schema_suggestions_status', table_name='schema_suggestions') + op.drop_index('idx_schema_suggestions_schema_type', table_name='schema_suggestions') + op.drop_index('idx_schema_suggestions_brand_status', table_name='schema_suggestions') + op.drop_index('idx_schema_suggestions_brand_id', table_name='schema_suggestions') + op.drop_table('schema_suggestions') + op.drop_index('idx_monitoring_records_status', table_name='monitoring_records') + op.drop_index('idx_monitoring_records_next_check_at', table_name='monitoring_records') + op.drop_index('idx_monitoring_records_brand_id', table_name='monitoring_records') + op.drop_table('monitoring_records') + op.drop_index('idx_diagnosis_records_user_id', table_name='diagnosis_records') + op.drop_index('idx_diagnosis_records_status', table_name='diagnosis_records') + op.drop_index('idx_diagnosis_records_created_at', table_name='diagnosis_records') + op.drop_index('idx_diagnosis_records_brand_id', table_name='diagnosis_records') + op.drop_table('diagnosis_records') + op.drop_index(op.f('ix_detection_tasks_user_id'), table_name='detection_tasks') + op.drop_index('idx_detection_tasks_user_id', table_name='detection_tasks') + op.drop_index('idx_detection_tasks_is_active', table_name='detection_tasks') + op.drop_index('idx_detection_tasks_brand_id', table_name='detection_tasks') + op.drop_table('detection_tasks') + op.drop_table('content_metrics') + op.drop_table('competitors') + op.drop_index('idx_competitor_insights_brand_id', table_name='competitor_insights') + op.drop_index('idx_competitor_insights_analysis_type', table_name='competitor_insights') + op.drop_table('competitor_insights') + op.drop_index(op.f('ix_alerts_user_id'), table_name='alerts') + op.drop_index('idx_alerts_user_read', table_name='alerts') + op.drop_index('idx_alerts_user_id', table_name='alerts') + op.drop_index('idx_alerts_is_read', table_name='alerts') + op.drop_index('idx_alerts_created_at', table_name='alerts') + op.drop_index('idx_alerts_brand_id', table_name='alerts') + op.drop_index('idx_alerts_alert_type', table_name='alerts') + op.drop_table('alerts') + op.drop_index(op.f('ix_alert_settings_user_id'), table_name='alert_settings') + op.drop_index('idx_alert_settings_user_id', table_name='alert_settings') + op.drop_index('idx_alert_settings_brand_type', table_name='alert_settings') + op.drop_index('idx_alert_settings_brand_id', table_name='alert_settings') + op.drop_table('alert_settings') + op.drop_index(op.f('ix_publish_records_organization_id'), table_name='publish_records') + op.drop_table('publish_records') + op.drop_index('idx_platform_rules_rule_category', table_name='platform_rules') + op.drop_index('idx_platform_rules_platform_category', table_name='platform_rules') + op.drop_index('idx_platform_rules_platform', table_name='platform_rules') + op.drop_index('idx_platform_rules_is_active', table_name='platform_rules') + op.drop_table('platform_rules') + op.drop_index(op.f('ix_platform_rule_versions_rule_id'), table_name='platform_rule_versions') + op.drop_index('idx_rule_versions_rule_id', table_name='platform_rule_versions') + op.drop_index('idx_rule_versions_platform', table_name='platform_rule_versions') + op.drop_table('platform_rule_versions') + op.drop_index('idx_organizations_slug', table_name='organizations') + op.drop_table('organizations') + op.drop_index(op.f('ix_optimization_insights_organization_id'), table_name='optimization_insights') + op.drop_table('optimization_insights') + op.drop_table('brands') + op.drop_index(op.f('ix_api_keys_user_id'), table_name='api_keys') + op.drop_index('idx_api_keys_user_engine', table_name='api_keys') + op.drop_index('idx_api_keys_engine_status', table_name='api_keys') + op.drop_table('api_keys') + op.drop_index('idx_agent_registry_status', table_name='agent_registry') + op.drop_index('idx_agent_registry_name', table_name='agent_registry') + op.drop_index('idx_agent_registry_agent_type', table_name='agent_registry') + op.drop_table('agent_registry') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/a7b9c1d3ef67_add_distribution_schedules_table.py b/backend/alembic/versions/a7b9c1d3ef67_add_distribution_schedules_table.py deleted file mode 100644 index f48a7af..0000000 --- a/backend/alembic/versions/a7b9c1d3ef67_add_distribution_schedules_table.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Add distribution_schedules table - -Revision ID: a7b9c1d3ef67 -Revises: f6g8h0i2de56 -Create Date: 2026-05-23 15:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'a7b9c1d3ef67' -down_revision: Union[str, Sequence[str], None] = 'f6g8h0i2de56' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Add distribution_schedules table.""" - op.create_table( - 'distribution_schedules', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('content_title', sa.String(500), nullable=False), - sa.Column('content_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('platforms', postgresql.JSONB(), nullable=True), - sa.Column('tips', postgresql.JSONB(), nullable=True), - sa.Column('status', sa.String(20), server_default='pending', nullable=False), - sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['content_id'], ['contents.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL') - ) - op.create_index('idx_distribution_schedules_organization_id', 'distribution_schedules', ['organization_id']) - op.create_index('idx_distribution_schedules_status', 'distribution_schedules', ['status']) - op.create_index('idx_distribution_schedules_created_by', 'distribution_schedules', ['created_by']) - - -def downgrade() -> None: - """Remove distribution_schedules table.""" - op.drop_index('idx_distribution_schedules_created_by', table_name='distribution_schedules') - op.drop_index('idx_distribution_schedules_status', table_name='distribution_schedules') - op.drop_index('idx_distribution_schedules_organization_id', table_name='distribution_schedules') - op.drop_table('distribution_schedules') diff --git a/backend/alembic/versions/b2c4d6e8fa10_add_confidence_match_type_to_citation_records.py b/backend/alembic/versions/b2c4d6e8fa10_add_confidence_match_type_to_citation_records.py deleted file mode 100644 index 25d6e22..0000000 --- a/backend/alembic/versions/b2c4d6e8fa10_add_confidence_match_type_to_citation_records.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Add confidence and match_type to citation_records - -Revision ID: b2c4d6e8fa10 -Revises: 488d0bd5ab01 -Create Date: 2026-04-23 16:10:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'b2c4d6e8fa10' -down_revision: Union[str, Sequence[str], None] = '488d0bd5ab01' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Add confidence and match_type columns to citation_records.""" - op.add_column( - 'citation_records', - sa.Column('confidence', sa.Float(), nullable=True) - ) - op.add_column( - 'citation_records', - sa.Column('match_type', sa.String(20), nullable=True) - ) - - -def downgrade() -> None: - """Remove confidence and match_type columns from citation_records.""" - op.drop_column('citation_records', 'match_type') - op.drop_column('citation_records', 'confidence') diff --git a/backend/alembic/versions/c3d5e7f9ab12_add_user_management_fields.py b/backend/alembic/versions/c3d5e7f9ab12_add_user_management_fields.py deleted file mode 100644 index 8c85bdc..0000000 --- a/backend/alembic/versions/c3d5e7f9ab12_add_user_management_fields.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Add user management fields - -Revision ID: c3d5e7f9ab12 -Revises: b2c4d6e8fa10 -Create Date: 2026-04-24 10:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'c3d5e7f9ab12' -down_revision: Union[str, Sequence[str], None] = 'b2c4d6e8fa10' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Add user management fields to users table.""" - op.add_column('users', sa.Column('email_verified', sa.Boolean(), server_default='false', nullable=False)) - op.add_column('users', sa.Column('verification_code', sa.String(6), nullable=True)) - op.add_column('users', sa.Column('verification_code_expires', sa.DateTime(), nullable=True)) - op.add_column('users', sa.Column('reset_token', sa.String(255), nullable=True)) - op.add_column('users', sa.Column('reset_token_expires', sa.DateTime(), nullable=True)) - op.add_column('users', sa.Column('avatar_url', sa.String(500), nullable=True)) - op.add_column('users', sa.Column('is_admin', sa.Boolean(), server_default='false', nullable=False)) - - -def downgrade() -> None: - """Remove user management fields from users table.""" - op.drop_column('users', 'is_admin') - op.drop_column('users', 'avatar_url') - op.drop_column('users', 'reset_token_expires') - op.drop_column('users', 'reset_token') - op.drop_column('users', 'verification_code_expires') - op.drop_column('users', 'verification_code') - op.drop_column('users', 'email_verified') diff --git a/backend/alembic/versions/d4f6g8h0ab23_add_geo_lifecycle_tables.py b/backend/alembic/versions/d4f6g8h0ab23_add_geo_lifecycle_tables.py deleted file mode 100644 index cc1873a..0000000 --- a/backend/alembic/versions/d4f6g8h0ab23_add_geo_lifecycle_tables.py +++ /dev/null @@ -1,397 +0,0 @@ -"""Add GEO lifecycle tables - -Revision ID: d4f6g8h0ab23 -Revises: c3d5e7f9ab12 -Create Date: 2026-05-23 10:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'd4f6g8h0ab23' -down_revision: Union[str, Sequence[str], None] = 'c3d5e7f9ab12' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Add organizations, lifecycle, agent, content, platform_rules, brand_knowledge, keywords tables and modify users.""" - - # 1. organizations table (must be created first as other tables reference it) - op.create_table( - 'organizations', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('name', sa.String(100), nullable=False), - sa.Column('slug', sa.String(50), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('logo_url', sa.String(500), nullable=True), - sa.Column('plan', sa.String(20), server_default='free', nullable=False), - sa.Column('max_members', sa.Integer(), server_default='5', nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('slug') - ) - op.create_index('idx_organizations_slug', 'organizations', ['slug']) - - # 2. Modify users table - add organization_id and role - op.add_column('users', sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=True)) - op.add_column('users', sa.Column('role', sa.String(20), server_default='owner', nullable=False)) - op.create_foreign_key('fk_users_organization_id', 'users', 'organizations', ['organization_id'], ['id'], ondelete='SET NULL') - op.create_index('idx_users_organization_id', 'users', ['organization_id']) - - # 3. org_members table - op.create_table( - 'org_members', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('role', sa.String(20), server_default='viewer', nullable=False), - sa.Column('joined_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('invited_by', postgresql.UUID(as_uuid=True), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE') - ) - op.create_index('idx_org_members_organization_id', 'org_members', ['organization_id']) - op.create_index('idx_org_members_user_id', 'org_members', ['user_id']) - op.create_index('idx_org_members_org_user', 'org_members', ['organization_id', 'user_id'], unique=True) - - # 4. lifecycle_projects table - op.create_table( - 'lifecycle_projects', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('brand_name', sa.String(100), nullable=False), - sa.Column('brand_aliases', postgresql.JSONB(), server_default='[]', nullable=False), - sa.Column('current_stage', sa.Integer(), server_default='1', nullable=False), - sa.Column('status', sa.String(20), server_default='active', nullable=False), - sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL') - ) - op.create_index('idx_lifecycle_projects_organization_id', 'lifecycle_projects', ['organization_id']) - op.create_index('idx_lifecycle_projects_status', 'lifecycle_projects', ['status']) - op.create_index('idx_lifecycle_projects_brand_name', 'lifecycle_projects', ['brand_name']) - - # 5. project_stages table - op.create_table( - 'project_stages', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('stage_number', sa.Integer(), nullable=False), - sa.Column('status', sa.String(20), server_default='pending', nullable=False), - sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('metrics', postgresql.JSONB(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['project_id'], ['lifecycle_projects.id'], ondelete='CASCADE') - ) - op.create_index('idx_project_stages_project_id', 'project_stages', ['project_id']) - op.create_index('idx_project_stages_status', 'project_stages', ['status']) - op.create_index('idx_project_stages_project_stage', 'project_stages', ['project_id', 'stage_number'], unique=True) - - # 6. agent_registry table - op.create_table( - 'agent_registry', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('name', sa.String(50), nullable=False), - sa.Column('display_name', sa.String(100), nullable=True), - sa.Column('agent_type', sa.String(50), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('version', sa.String(20), nullable=True), - sa.Column('endpoint', sa.String(500), nullable=True), - sa.Column('status', sa.String(20), server_default='offline', nullable=False), - sa.Column('capabilities', postgresql.JSONB(), nullable=True), - sa.Column('last_heartbeat', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') - ) - op.create_index('idx_agent_registry_name', 'agent_registry', ['name']) - op.create_index('idx_agent_registry_agent_type', 'agent_registry', ['agent_type']) - op.create_index('idx_agent_registry_status', 'agent_registry', ['status']) - - # 7. agent_configs table - op.create_table( - 'agent_configs', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('config_key', sa.String(100), nullable=False), - sa.Column('config_value', postgresql.JSONB(), nullable=False), - sa.Column('description', sa.String(500), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_by', postgresql.UUID(as_uuid=True), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['agent_id'], ['agent_registry.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['updated_by'], ['users.id'], ondelete='SET NULL') - ) - op.create_index('idx_agent_configs_agent_id', 'agent_configs', ['agent_id']) - op.create_index('idx_agent_configs_agent_key', 'agent_configs', ['agent_id', 'config_key'], unique=True) - - # 8. agent_tasks table - op.create_table( - 'agent_tasks', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('task_type', sa.String(50), nullable=False), - sa.Column('status', sa.String(20), server_default='pending', nullable=False), - sa.Column('priority', sa.Integer(), server_default='0', nullable=False), - sa.Column('input_data', postgresql.JSONB(), nullable=True), - sa.Column('output_data', postgresql.JSONB(), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('scheduled_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['agent_id'], ['agent_registry.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['project_id'], ['lifecycle_projects.id'], ondelete='SET NULL') - ) - op.create_index('idx_agent_tasks_agent_id', 'agent_tasks', ['agent_id']) - op.create_index('idx_agent_tasks_status', 'agent_tasks', ['status']) - op.create_index('idx_agent_tasks_organization_id', 'agent_tasks', ['organization_id']) - op.create_index('idx_agent_tasks_project_id', 'agent_tasks', ['project_id']) - op.create_index('idx_agent_tasks_created_by', 'agent_tasks', ['created_by']) - op.create_index('idx_agent_tasks_task_type', 'agent_tasks', ['task_type']) - - # 9. agent_task_logs table - op.create_table( - 'agent_task_logs', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('task_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('log_level', sa.String(10), nullable=False), - sa.Column('message', sa.Text(), nullable=False), - sa.Column('metadata', postgresql.JSONB(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['task_id'], ['agent_tasks.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['agent_id'], ['agent_registry.id'], ondelete='CASCADE') - ) - op.create_index('idx_agent_task_logs_task_id', 'agent_task_logs', ['task_id']) - op.create_index('idx_agent_task_logs_agent_id', 'agent_task_logs', ['agent_id']) - op.create_index('idx_agent_task_logs_created_at', 'agent_task_logs', ['created_at']) - - # 10. contents table - op.create_table( - 'contents', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('title', sa.String(500), nullable=False), - sa.Column('content_type', sa.String(50), nullable=False), - sa.Column('body', sa.Text(), nullable=True), - sa.Column('status', sa.String(20), server_default='draft', nullable=False), - sa.Column('target_platforms', postgresql.JSONB(), nullable=True), - sa.Column('keywords', postgresql.JSONB(), nullable=True), - sa.Column('metadata', postgresql.JSONB(), nullable=True), - sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('current_version', sa.Integer(), server_default='1', nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['project_id'], ['lifecycle_projects.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL') - ) - op.create_index('idx_contents_organization_id', 'contents', ['organization_id']) - op.create_index('idx_contents_project_id', 'contents', ['project_id']) - op.create_index('idx_contents_status', 'contents', ['status']) - op.create_index('idx_contents_content_type', 'contents', ['content_type']) - op.create_index('idx_contents_created_by', 'contents', ['created_by']) - - # 11. content_versions table - op.create_table( - 'content_versions', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('content_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('version_number', sa.Integer(), nullable=False), - sa.Column('title', sa.String(500), nullable=True), - sa.Column('body', sa.Text(), nullable=True), - sa.Column('change_summary', sa.String(500), nullable=True), - sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['content_id'], ['contents.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL') - ) - op.create_index('idx_content_versions_content_id', 'content_versions', ['content_id']) - op.create_index('idx_content_versions_content_version', 'content_versions', ['content_id', 'version_number'], unique=True) - - # 12. content_reviews table - op.create_table( - 'content_reviews', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('content_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('reviewer_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('status', sa.String(20), nullable=False), - sa.Column('comments', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['content_id'], ['contents.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['reviewer_id'], ['users.id'], ondelete='CASCADE') - ) - op.create_index('idx_content_reviews_content_id', 'content_reviews', ['content_id']) - op.create_index('idx_content_reviews_reviewer_id', 'content_reviews', ['reviewer_id']) - - # 13. platform_rules table - op.create_table( - 'platform_rules', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('platform', sa.String(50), nullable=False), - sa.Column('rule_category', sa.String(50), nullable=False), - sa.Column('rule_name', sa.String(200), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('check_criteria', postgresql.JSONB(), nullable=True), - sa.Column('severity', sa.String(20), nullable=False), - sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('idx_platform_rules_platform', 'platform_rules', ['platform']) - op.create_index('idx_platform_rules_rule_category', 'platform_rules', ['rule_category']) - op.create_index('idx_platform_rules_is_active', 'platform_rules', ['is_active']) - op.create_index('idx_platform_rules_platform_category', 'platform_rules', ['platform', 'rule_category']) - - # 14. brand_knowledge table - op.create_table( - 'brand_knowledge', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('category', sa.String(50), nullable=False), - sa.Column('title', sa.String(200), nullable=False), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('metadata', postgresql.JSONB(), nullable=True), - sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False), - sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL') - ) - op.create_index('idx_brand_knowledge_organization_id', 'brand_knowledge', ['organization_id']) - op.create_index('idx_brand_knowledge_category', 'brand_knowledge', ['category']) - op.create_index('idx_brand_knowledge_is_active', 'brand_knowledge', ['is_active']) - - # 15. keywords table - op.create_table( - 'keywords', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('keyword', sa.String(200), nullable=False), - sa.Column('category', sa.String(50), nullable=False), - sa.Column('priority', sa.Integer(), server_default='0', nullable=False), - sa.Column('search_volume', sa.Integer(), nullable=True), - sa.Column('competition_level', sa.String(20), nullable=True), - sa.Column('status', sa.String(20), server_default='active', nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['project_id'], ['lifecycle_projects.id'], ondelete='SET NULL') - ) - op.create_index('idx_keywords_organization_id', 'keywords', ['organization_id']) - op.create_index('idx_keywords_project_id', 'keywords', ['project_id']) - op.create_index('idx_keywords_category', 'keywords', ['category']) - op.create_index('idx_keywords_status', 'keywords', ['status']) - op.create_index('idx_keywords_keyword', 'keywords', ['keyword']) - - -def downgrade() -> None: - """Remove all GEO lifecycle tables and revert users table changes.""" - - # Drop tables in reverse order (respecting foreign key dependencies) - op.drop_index('idx_keywords_keyword', table_name='keywords') - op.drop_index('idx_keywords_status', table_name='keywords') - op.drop_index('idx_keywords_category', table_name='keywords') - op.drop_index('idx_keywords_project_id', table_name='keywords') - op.drop_index('idx_keywords_organization_id', table_name='keywords') - op.drop_table('keywords') - - op.drop_index('idx_brand_knowledge_is_active', table_name='brand_knowledge') - op.drop_index('idx_brand_knowledge_category', table_name='brand_knowledge') - op.drop_index('idx_brand_knowledge_organization_id', table_name='brand_knowledge') - op.drop_table('brand_knowledge') - - op.drop_index('idx_platform_rules_platform_category', table_name='platform_rules') - op.drop_index('idx_platform_rules_is_active', table_name='platform_rules') - op.drop_index('idx_platform_rules_rule_category', table_name='platform_rules') - op.drop_index('idx_platform_rules_platform', table_name='platform_rules') - op.drop_table('platform_rules') - - op.drop_index('idx_content_reviews_reviewer_id', table_name='content_reviews') - op.drop_index('idx_content_reviews_content_id', table_name='content_reviews') - op.drop_table('content_reviews') - - op.drop_index('idx_content_versions_content_version', table_name='content_versions') - op.drop_index('idx_content_versions_content_id', table_name='content_versions') - op.drop_table('content_versions') - - op.drop_index('idx_contents_created_by', table_name='contents') - op.drop_index('idx_contents_content_type', table_name='contents') - op.drop_index('idx_contents_status', table_name='contents') - op.drop_index('idx_contents_project_id', table_name='contents') - op.drop_index('idx_contents_organization_id', table_name='contents') - op.drop_table('contents') - - op.drop_index('idx_agent_task_logs_created_at', table_name='agent_task_logs') - op.drop_index('idx_agent_task_logs_agent_id', table_name='agent_task_logs') - op.drop_index('idx_agent_task_logs_task_id', table_name='agent_task_logs') - op.drop_table('agent_task_logs') - - op.drop_index('idx_agent_tasks_task_type', table_name='agent_tasks') - op.drop_index('idx_agent_tasks_created_by', table_name='agent_tasks') - op.drop_index('idx_agent_tasks_project_id', table_name='agent_tasks') - op.drop_index('idx_agent_tasks_organization_id', table_name='agent_tasks') - op.drop_index('idx_agent_tasks_status', table_name='agent_tasks') - op.drop_index('idx_agent_tasks_agent_id', table_name='agent_tasks') - op.drop_table('agent_tasks') - - op.drop_index('idx_agent_configs_agent_key', table_name='agent_configs') - op.drop_index('idx_agent_configs_agent_id', table_name='agent_configs') - op.drop_table('agent_configs') - - op.drop_index('idx_agent_registry_status', table_name='agent_registry') - op.drop_index('idx_agent_registry_agent_type', table_name='agent_registry') - op.drop_index('idx_agent_registry_name', table_name='agent_registry') - op.drop_table('agent_registry') - - op.drop_index('idx_project_stages_project_stage', table_name='project_stages') - op.drop_index('idx_project_stages_status', table_name='project_stages') - op.drop_index('idx_project_stages_project_id', table_name='project_stages') - op.drop_table('project_stages') - - op.drop_index('idx_lifecycle_projects_brand_name', table_name='lifecycle_projects') - op.drop_index('idx_lifecycle_projects_status', table_name='lifecycle_projects') - op.drop_index('idx_lifecycle_projects_organization_id', table_name='lifecycle_projects') - op.drop_table('lifecycle_projects') - - op.drop_index('idx_org_members_org_user', table_name='org_members') - op.drop_index('idx_org_members_user_id', table_name='org_members') - op.drop_index('idx_org_members_organization_id', table_name='org_members') - op.drop_table('org_members') - - # Revert users table changes - op.drop_index('idx_users_organization_id', table_name='users') - op.drop_constraint('fk_users_organization_id', 'users', type_='foreignkey') - op.drop_column('users', 'role') - op.drop_column('users', 'organization_id') - - op.drop_index('idx_organizations_slug', table_name='organizations') - op.drop_table('organizations') diff --git a/backend/alembic/versions/e5f7a9b1cd34_add_alerts_and_alert_settings_tables.py b/backend/alembic/versions/e5f7a9b1cd34_add_alerts_and_alert_settings_tables.py deleted file mode 100644 index d8f075c..0000000 --- a/backend/alembic/versions/e5f7a9b1cd34_add_alerts_and_alert_settings_tables.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Add alerts and alert_settings tables - -Revision ID: e5f7a9b1cd34 -Revises: 8ccb553ff975 -Create Date: 2026-05-20 10:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'e5f7a9b1cd34' -down_revision: Union[str, Sequence[str], None] = '8ccb553ff975' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Create alerts and alert_settings tables.""" - # 创建 alerts 表 - op.create_table( - 'alerts', - sa.Column('id', sa.Uuid(as_uuid=True), primary_key=True), - sa.Column('brand_id', sa.Uuid(as_uuid=True), - sa.ForeignKey('brands.id', ondelete='CASCADE'), nullable=False), - sa.Column('user_id', sa.Uuid(as_uuid=True), nullable=False), - sa.Column('alert_type', sa.String(50), nullable=False, - comment='告警类型: score_drop / score_rise / negative_sentiment / competitor_overtake / new_platform_mention'), - sa.Column('severity', sa.String(20), nullable=False, - comment='严重程度: critical / warning / info'), - sa.Column('title', sa.String(200), nullable=False), - sa.Column('message', sa.Text(), nullable=False), - sa.Column('data', sa.JSON(), nullable=True, - comment='告警相关数据(JSON)'), - sa.Column('is_read', sa.Boolean(), default=False, nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=False), - ) - - # 创建 alerts 表的索引 - op.create_index('idx_alerts_user_id', 'alerts', ['user_id']) - op.create_index('idx_alerts_brand_id', 'alerts', ['brand_id']) - op.create_index('idx_alerts_alert_type', 'alerts', ['alert_type']) - op.create_index('idx_alerts_is_read', 'alerts', ['is_read']) - op.create_index('idx_alerts_created_at', 'alerts', ['created_at']) - op.create_index('idx_alerts_user_read', 'alerts', ['user_id', 'is_read']) - - # 创建 alert_settings 表 - op.create_table( - 'alert_settings', - sa.Column('id', sa.Uuid(as_uuid=True), primary_key=True), - sa.Column('brand_id', sa.Uuid(as_uuid=True), - sa.ForeignKey('brands.id', ondelete='CASCADE'), nullable=False), - sa.Column('user_id', sa.Uuid(as_uuid=True), nullable=False), - sa.Column('alert_type', sa.String(50), nullable=False, - comment='告警类型: score_drop / score_rise / negative_sentiment / competitor_overtake / new_platform_mention'), - sa.Column('enabled', sa.Boolean(), default=True, nullable=False), - sa.Column('threshold', sa.Float(), nullable=True, - comment='阈值(如评分下降超过5分触发)'), - sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), - ) - - # 创建 alert_settings 表的索引 - op.create_index('idx_alert_settings_brand_id', 'alert_settings', ['brand_id']) - op.create_index('idx_alert_settings_user_id', 'alert_settings', ['user_id']) - op.create_index('idx_alert_settings_brand_type', 'alert_settings', ['brand_id', 'alert_type'], unique=True) - - -def downgrade() -> None: - """Drop alerts and alert_settings tables.""" - op.drop_index('idx_alert_settings_brand_type', table_name='alert_settings') - op.drop_index('idx_alert_settings_user_id', table_name='alert_settings') - op.drop_index('idx_alert_settings_brand_id', table_name='alert_settings') - op.drop_table('alert_settings') - - op.drop_index('idx_alerts_user_read', table_name='alerts') - op.drop_index('idx_alerts_created_at', table_name='alerts') - op.drop_index('idx_alerts_is_read', table_name='alerts') - op.drop_index('idx_alerts_alert_type', table_name='alerts') - op.drop_index('idx_alerts_brand_id', table_name='alerts') - op.drop_index('idx_alerts_user_id', table_name='alerts') - op.drop_table('alerts') diff --git a/backend/alembic/versions/e5f7a9b1cd35_add_suggestions_table.py b/backend/alembic/versions/e5f7a9b1cd35_add_suggestions_table.py deleted file mode 100644 index 81c67c3..0000000 --- a/backend/alembic/versions/e5f7a9b1cd35_add_suggestions_table.py +++ /dev/null @@ -1,50 +0,0 @@ -"""add suggestions table - -Revision ID: e5f7a9b1cd35 -Revises: e5f7a9b1cd34 -Create Date: 2025-01-20 10:00:00.000000 -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import UUID, JSONB - -# revision identifiers -revision = "e5f7a9b1cd35" -down_revision = "e5f7a9b1cd34" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.create_table( - "suggestions", - sa.Column("id", UUID(as_uuid=True), primary_key=True), - sa.Column("brand_id", UUID(as_uuid=True), sa.ForeignKey("brands.id", ondelete="CASCADE"), nullable=False), - sa.Column("type", sa.String(50), nullable=False, comment="建议类型"), - sa.Column("priority", sa.String(20), nullable=False, server_default="medium", comment="优先级"), - sa.Column("title", sa.String(200), nullable=False), - sa.Column("description", sa.Text, nullable=False), - sa.Column("action", sa.Text, nullable=True, comment="具体操作步骤"), - sa.Column("expected_impact", sa.String(200), nullable=True, comment="预期效果"), - sa.Column("difficulty", sa.String(20), nullable=False, server_default="medium", comment="难度"), - sa.Column("status", sa.String(20), nullable=False, server_default="pending", comment="状态"), - sa.Column("generated_at", sa.DateTime, server_default=sa.func.now(), nullable=False), - sa.Column("updated_at", sa.DateTime, server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), - sa.Column("batch_id", UUID(as_uuid=True), nullable=False), - sa.Column("source", sa.String(20), nullable=False, server_default="rule", comment="生成来源"), - ) - - op.create_index("idx_suggestions_brand_id", "suggestions", ["brand_id"]) - op.create_index("idx_suggestions_status", "suggestions", ["status"]) - op.create_index("idx_suggestions_type", "suggestions", ["type"]) - op.create_index("idx_suggestions_batch_id", "suggestions", ["batch_id"]) - op.create_index("idx_suggestions_brand_status", "suggestions", ["brand_id", "status"]) - - -def downgrade() -> None: - op.drop_index("idx_suggestions_brand_status", table_name="suggestions") - op.drop_index("idx_suggestions_batch_id", table_name="suggestions") - op.drop_index("idx_suggestions_type", table_name="suggestions") - op.drop_index("idx_suggestions_status", table_name="suggestions") - op.drop_index("idx_suggestions_brand_id", table_name="suggestions") - op.drop_table("suggestions") diff --git a/backend/alembic/versions/e5f7g9h1cd45_add_knowledge_base_tables.py b/backend/alembic/versions/e5f7g9h1cd45_add_knowledge_base_tables.py deleted file mode 100644 index e84bd23..0000000 --- a/backend/alembic/versions/e5f7g9h1cd45_add_knowledge_base_tables.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Add knowledge base tables with pgvector support - -Revision ID: e5f7g9h1cd45 -Revises: d4f6g8h0ab23 -Create Date: 2026-05-23 12:00:00.000000 - -""" -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = "e5f7g9h1cd45" -down_revision: Union[str, None] = "d4f6g8h0ab23" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ------------------------------------------------------------------ # - # 1. Enable pgvector extension - # ------------------------------------------------------------------ # - op.execute("CREATE EXTENSION IF NOT EXISTS vector") - - # ------------------------------------------------------------------ # - # 2. knowledge_bases - # ------------------------------------------------------------------ # - op.create_table( - "knowledge_bases", - sa.Column( - "id", - postgresql.UUID(as_uuid=True), - primary_key=True, - nullable=False, - ), - sa.Column( - "organization_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("organizations.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("name", sa.String(200), nullable=False), - sa.Column("type", sa.String(20), nullable=False), - sa.Column("description", sa.Text, nullable=True), - sa.Column("document_count", sa.Integer, server_default="0", nullable=False), - sa.Column("status", sa.String(20), server_default="active", nullable=False), - sa.Column( - "created_by", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("users.id", ondelete="SET NULL"), - nullable=True, - ), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - ) - op.create_index("idx_knowledge_bases_organization_id", "knowledge_bases", ["organization_id"]) - op.create_index("idx_knowledge_bases_type", "knowledge_bases", ["type"]) - op.create_index("idx_knowledge_bases_status", "knowledge_bases", ["status"]) - - # ------------------------------------------------------------------ # - # 3. knowledge_documents - # ------------------------------------------------------------------ # - op.create_table( - "knowledge_documents", - sa.Column( - "id", - postgresql.UUID(as_uuid=True), - primary_key=True, - nullable=False, - ), - sa.Column( - "knowledge_base_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("knowledge_bases.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("title", sa.String(500), nullable=False), - sa.Column("source_type", sa.String(20), nullable=False), - sa.Column("source_url", sa.String(2000), nullable=True), - sa.Column("content", sa.Text, nullable=False), - sa.Column("content_hash", sa.String(64), nullable=False), - sa.Column("chunk_count", sa.Integer, server_default="0", nullable=False), - sa.Column("status", sa.String(20), server_default="processing", nullable=False), - sa.Column("error_message", sa.Text, nullable=True), - sa.Column("metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - ) - op.create_index( - "idx_knowledge_documents_knowledge_base_id", - "knowledge_documents", - ["knowledge_base_id"], - ) - op.create_index("idx_knowledge_documents_status", "knowledge_documents", ["status"]) - op.create_index( - "idx_knowledge_documents_content_hash", "knowledge_documents", ["content_hash"] - ) - - # ------------------------------------------------------------------ # - # 4. knowledge_chunks (embedding column via raw SQL for vector type) - # ------------------------------------------------------------------ # - op.create_table( - "knowledge_chunks", - sa.Column( - "id", - postgresql.UUID(as_uuid=True), - primary_key=True, - nullable=False, - ), - sa.Column( - "document_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("knowledge_documents.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("content", sa.Text, nullable=False), - sa.Column("chunk_index", sa.Integer, nullable=False), - sa.Column("token_count", sa.Integer, server_default="0", nullable=False), - sa.Column("metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - ) - - # Add vector embedding column via raw SQL (pgvector type not in SA dialect) - # Dimension 1536 matches OpenAI text-embedding-3-small - op.execute( - "ALTER TABLE knowledge_chunks ADD COLUMN embedding vector(1536)" - ) - - op.create_index("idx_knowledge_chunks_document_id", "knowledge_chunks", ["document_id"]) - op.create_index( - "idx_knowledge_chunks_chunk_index", - "knowledge_chunks", - ["document_id", "chunk_index"], - ) - - # HNSW index for approximate nearest-neighbor cosine similarity search - op.execute( - "CREATE INDEX ix_knowledge_chunks_embedding " - "ON knowledge_chunks USING hnsw (embedding vector_cosine_ops)" - ) - - # ------------------------------------------------------------------ # - # 5. knowledge_search_logs - # ------------------------------------------------------------------ # - op.create_table( - "knowledge_search_logs", - sa.Column( - "id", - postgresql.UUID(as_uuid=True), - primary_key=True, - nullable=False, - ), - sa.Column( - "organization_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("organizations.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column( - "user_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("users.id", ondelete="SET NULL"), - nullable=True, - ), - sa.Column("query", sa.Text, nullable=False), - sa.Column("knowledge_base_ids", postgresql.JSONB(astext_type=sa.Text()), nullable=True), - sa.Column("results_count", sa.Integer, server_default="0", nullable=False), - sa.Column("latency_ms", sa.Integer, server_default="0", nullable=False), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - ) - op.create_index( - "idx_knowledge_search_logs_organization_id", - "knowledge_search_logs", - ["organization_id"], - ) - op.create_index( - "idx_knowledge_search_logs_user_id", "knowledge_search_logs", ["user_id"] - ) - op.create_index( - "idx_knowledge_search_logs_created_at", "knowledge_search_logs", ["created_at"] - ) - - -def downgrade() -> None: - # Drop tables in reverse dependency order - op.drop_table("knowledge_search_logs") - op.execute("DROP INDEX IF EXISTS ix_knowledge_chunks_embedding") - op.drop_table("knowledge_chunks") - op.drop_table("knowledge_documents") - op.drop_table("knowledge_bases") - # Note: we do NOT drop the vector extension as other tables might rely on it diff --git a/backend/alembic/versions/f063b3da67b6_add_payment_order_attribution_record_.py b/backend/alembic/versions/f063b3da67b6_add_payment_order_attribution_record_.py deleted file mode 100644 index aa2883f..0000000 --- a/backend/alembic/versions/f063b3da67b6_add_payment_order_attribution_record_.py +++ /dev/null @@ -1,131 +0,0 @@ -"""add monetization tables: diagnosis_records, attribution_records, payment_orders - -Revision ID: f063b3da67b6 -Revises: g1h2i3j4kl56 -Create Date: 2026-06-01 07:40:07.419407 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -revision: str = "f063b3da67b6" -down_revision: Union[str, Sequence[str], None] = "g1h2i3j4kl56" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.create_table( - "diagnosis_records", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("brand_id", sa.Uuid(), nullable=False), - sa.Column("user_id", sa.Uuid(), nullable=False), - sa.Column("diagnosis_type", sa.String(length=20), nullable=False), - sa.Column("status", sa.String(length=20), nullable=False), - sa.Column("overall_score", sa.Float(), nullable=True), - sa.Column("result_json", sa.JSON(), nullable=True), - sa.Column("error_message", sa.Text(), nullable=True), - sa.Column("collection_metadata", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), - sa.Column("completed_at", sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(["brand_id"], ["brands.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("idx_diagnosis_records_brand_id", "diagnosis_records", ["brand_id"]) - op.create_index("idx_diagnosis_records_user_id", "diagnosis_records", ["user_id"]) - op.create_index("idx_diagnosis_records_status", "diagnosis_records", ["status"]) - op.create_index("idx_diagnosis_records_created_at", "diagnosis_records", ["created_at"]) - - op.create_table( - "attribution_records", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("user_id", sa.Text(), nullable=False), - sa.Column("brand_id", sa.Uuid(), nullable=False), - sa.Column("content_id", sa.Uuid(), nullable=True), - sa.Column("baseline_score", sa.Float(), nullable=False), - sa.Column("current_score", sa.Float(), nullable=True), - sa.Column("score_delta", sa.Float(), nullable=True), - sa.Column("attribution_window_days", sa.Integer(), server_default="28", nullable=False), - sa.Column("published_at", sa.DateTime(), nullable=True), - sa.Column("window_end_at", sa.DateTime(), nullable=True), - sa.Column("status", sa.String(length=20), server_default="tracking", nullable=False), - sa.Column("attributed_dimensions", sa.JSON(), nullable=True), - sa.Column("roi_percentage", sa.Float(), nullable=True), - sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), - sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), - sa.ForeignKeyConstraint(["brand_id"], ["brands.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["content_id"], ["contents.id"], ondelete="SET NULL"), - sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("idx_attribution_records_brand_id", "attribution_records", ["brand_id"]) - op.create_index("idx_attribution_records_user_id", "attribution_records", ["user_id"]) - op.create_index("idx_attribution_records_status", "attribution_records", ["status"]) - op.create_index("idx_attribution_records_content_id", "attribution_records", ["content_id"]) - - op.drop_table("payment_orders") - op.create_table( - "payment_orders", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("user_id", sa.String(length=36), nullable=False), - sa.Column("plan", sa.String(length=20), nullable=False), - sa.Column("amount", sa.Float(), nullable=False), - sa.Column("currency", sa.String(length=10), nullable=False), - sa.Column("payment_provider", sa.String(length=20), nullable=False), - sa.Column("payment_id", sa.String(length=255), nullable=True), - sa.Column("status", sa.String(length=20), nullable=False), - sa.Column("pay_url", sa.String(length=1024), nullable=True), - sa.Column("callback_data", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), - sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), - sa.Column("paid_at", sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - - -def downgrade() -> None: - op.drop_table("payment_orders") - op.create_table( - "payment_orders", - sa.Column("id", sa.TEXT(), nullable=False), - sa.Column("orderNo", sa.TEXT(), nullable=False), - sa.Column("userId", sa.TEXT(), nullable=False), - sa.Column("channelId", sa.TEXT(), nullable=True), - sa.Column("subject", sa.TEXT(), nullable=False), - sa.Column("body", sa.TEXT(), nullable=True), - sa.Column("amount", sa.NUMERIC(precision=12, scale=2), nullable=False), - sa.Column("currency", sa.TEXT(), server_default="'CNY'", nullable=False), - sa.Column("status", sa.TEXT(), server_default="'pending'", nullable=False), - sa.Column("clientIp", sa.TEXT(), nullable=True), - sa.Column("userAgent", sa.TEXT(), nullable=True), - sa.Column("metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=True), - sa.Column("channelOrderNo", sa.TEXT(), nullable=True), - sa.Column("createdAt", postgresql.TIMESTAMP(precision=3), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False), - sa.Column("updatedAt", postgresql.TIMESTAMP(precision=3), nullable=False), - sa.Column("paidAt", postgresql.TIMESTAMP(precision=3), nullable=True), - sa.Column("cancelledAt", postgresql.TIMESTAMP(precision=3), nullable=True), - sa.ForeignKeyConstraint(["channelId"], ["payment_channels.id"], onupdate="CASCADE", ondelete="SET NULL"), - sa.ForeignKeyConstraint(["userId"], ["users.id"], onupdate="CASCADE", ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("payment_orders_userId_idx"), "payment_orders", ["userId"]) - op.create_index(op.f("payment_orders_status_idx"), "payment_orders", ["status"]) - op.create_index(op.f("payment_orders_orderNo_key"), "payment_orders", ["orderNo"], unique=True) - op.create_index(op.f("payment_orders_orderNo_idx"), "payment_orders", ["orderNo"]) - op.create_index(op.f("payment_orders_createdAt_idx"), "payment_orders", ["createdAt"]) - - op.drop_index("idx_attribution_records_content_id", table_name="attribution_records") - op.drop_index("idx_attribution_records_status", table_name="attribution_records") - op.drop_index("idx_attribution_records_user_id", table_name="attribution_records") - op.drop_index("idx_attribution_records_brand_id", table_name="attribution_records") - op.drop_table("attribution_records") - - op.drop_index("idx_diagnosis_records_created_at", table_name="diagnosis_records") - op.drop_index("idx_diagnosis_records_status", table_name="diagnosis_records") - op.drop_index("idx_diagnosis_records_user_id", table_name="diagnosis_records") - op.drop_index("idx_diagnosis_records_brand_id", table_name="diagnosis_records") - op.drop_table("diagnosis_records") diff --git a/backend/alembic/versions/f6g8h0i2de56_add_analytics_tables.py b/backend/alembic/versions/f6g8h0i2de56_add_analytics_tables.py deleted file mode 100644 index 61379d7..0000000 --- a/backend/alembic/versions/f6g8h0i2de56_add_analytics_tables.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Add analytics tables (publish_records, content_metrics, optimization_insights) - -Revision ID: f6g8h0i2de56 -Revises: e5f7g9h1cd45 -Create Date: 2026-05-23 14:00:00.000000 - -""" -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "f6g8h0i2de56" -down_revision: Union[str, None] = "e5f7g9h1cd45" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ------------------------------------------------------------------ # - # 1. publish_records - # ------------------------------------------------------------------ # - op.create_table( - "publish_records", - sa.Column("id", sa.String(36), primary_key=True, nullable=False), - sa.Column("organization_id", sa.String(36), nullable=False), - sa.Column("content_title", sa.String(200), nullable=False), - sa.Column("content_id", sa.String(36), nullable=True), - sa.Column("platform", sa.String(50), nullable=False), - sa.Column("published_url", sa.String(500), nullable=True), - sa.Column("status", sa.String(20), server_default="draft", nullable=False), - sa.Column("published_at", sa.DateTime(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ), - ) - op.create_index("idx_publish_records_organization_id", "publish_records", ["organization_id"]) - op.create_index("idx_publish_records_platform", "publish_records", ["platform"]) - op.create_index("idx_publish_records_status", "publish_records", ["status"]) - op.create_index("idx_publish_records_created_at", "publish_records", ["created_at"]) - - # ------------------------------------------------------------------ # - # 2. content_metrics - # ------------------------------------------------------------------ # - op.create_table( - "content_metrics", - sa.Column("id", sa.String(36), primary_key=True, nullable=False), - sa.Column( - "publish_record_id", - sa.String(36), - sa.ForeignKey("publish_records.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column( - "recorded_at", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ), - # 互动指标 - sa.Column("views", sa.Integer(), server_default="0", nullable=False), - sa.Column("likes", sa.Integer(), server_default="0", nullable=False), - sa.Column("comments", sa.Integer(), server_default="0", nullable=False), - sa.Column("shares", sa.Integer(), server_default="0", nullable=False), - sa.Column("bookmarks", sa.Integer(), server_default="0", nullable=False), - # GEO核心指标 - sa.Column("ai_citation_count", sa.Integer(), server_default="0", nullable=False), - sa.Column("search_impressions", sa.Integer(), server_default="0", nullable=False), - sa.Column("search_clicks", sa.Integer(), server_default="0", nullable=False), - # 阅读指标 - sa.Column("avg_read_duration", sa.Float(), server_default="0.0", nullable=False), - sa.Column("read_completion_rate", sa.Float(), server_default="0.0", nullable=False), - ) - op.create_index( - "idx_content_metrics_publish_record_id", "content_metrics", ["publish_record_id"] - ) - op.create_index("idx_content_metrics_recorded_at", "content_metrics", ["recorded_at"]) - - # ------------------------------------------------------------------ # - # 3. optimization_insights - # ------------------------------------------------------------------ # - op.create_table( - "optimization_insights", - sa.Column("id", sa.String(36), primary_key=True, nullable=False), - sa.Column("organization_id", sa.String(36), nullable=False), - sa.Column("content_id", sa.String(36), nullable=True), - sa.Column("insight_type", sa.String(30), nullable=False), - sa.Column("title", sa.String(200), nullable=False), - sa.Column("description", sa.Text(), nullable=False), - sa.Column("recommendation", sa.Text(), nullable=False), - sa.Column("severity", sa.String(20), server_default="info", nullable=False), - sa.Column("applied", sa.Boolean(), server_default="false", nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("now()"), - nullable=False, - ), - ) - op.create_index( - "idx_optimization_insights_organization_id", - "optimization_insights", - ["organization_id"], - ) - op.create_index( - "idx_optimization_insights_insight_type", - "optimization_insights", - ["insight_type"], - ) - op.create_index( - "idx_optimization_insights_created_at", - "optimization_insights", - ["created_at"], - ) - - -def downgrade() -> None: - op.drop_table("optimization_insights") - op.drop_table("content_metrics") - op.drop_table("publish_records") diff --git a/backend/alembic/versions/f7a8b9c0de56_add_knowledge_graph_tables.py b/backend/alembic/versions/f7a8b9c0de56_add_knowledge_graph_tables.py deleted file mode 100644 index 3ec7dc1..0000000 --- a/backend/alembic/versions/f7a8b9c0de56_add_knowledge_graph_tables.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Add knowledge graph tables - -Revision ID: f7a8b9c0de56 -Revises: e5f7g9h1cd45 -Create Date: 2026-05-24 12:00:00.000000 - -""" -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op -from sqlalchemy.dialects import postgresql -import enum - -# revision identifiers, used by Alembic. -revision: str = "f7a8b9c0de56" -down_revision: Union[str, None] = "810a29804f5a" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ------------------------------------------------------------------ # - # 1. 创建实体类型枚举 - # ------------------------------------------------------------------ # - entity_type_enum = enum.Enum( - 'EntityType', - [ - 'ORGANIZATION', 'PRODUCT', 'PERSON', 'LOCATION', - 'TECHNOLOGY', 'BRAND', 'EVENT', 'CONCEPT', 'OTHER' - ] - ) - - # ------------------------------------------------------------------ # - # 2. 创建关系类型枚举 - # ------------------------------------------------------------------ # - relation_type_enum = enum.Enum( - 'RelationType', - [ - 'COMPETES_WITH', 'PARTNERS_WITH', 'ACQUIRES', 'SUBSIDIARY_OF', - 'PRODUCES', 'USES_TECHNOLOGY', 'PART_OF', - 'LOCATED_IN', 'FOUNDED_IN', - 'CEO_OF', 'FOUNDER_OF', - 'RELATED_TO', 'MENTIONED_IN', 'ALSO_KNOWN_AS' - ] - ) - - # ------------------------------------------------------------------ # - # 3. knowledge_entities 表 - # ------------------------------------------------------------------ # - op.create_table( - "knowledge_entities", - sa.Column( - "id", - postgresql.UUID(as_uuid=True), - primary_key=True, - nullable=False, - ), - sa.Column( - "knowledge_base_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("knowledge_bases.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("name", sa.String(500), nullable=False), - sa.Column("entity_type", sa.Enum(entity_type_enum, name="entitytype"), nullable=False), - sa.Column("description", sa.Text, nullable=True), - sa.Column("properties", postgresql.JSONB(astext_type=sa.Text()), nullable=True, server_default="{}"), - sa.Column( - "source_chunk_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("knowledge_chunks.id", ondelete="SET NULL"), - nullable=True, - ), - sa.Column("confidence", sa.String(20), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - ) - op.create_index("ix_entities_name", "knowledge_entities", ["name"]) - op.create_index("ix_entities_kb_name", "knowledge_entities", ["knowledge_base_id", "name"]) - op.create_index("ix_entities_kb_type", "knowledge_entities", ["knowledge_base_id", "entity_type"]) - - # ------------------------------------------------------------------ # - # 4. knowledge_relations 表 - # ------------------------------------------------------------------ # - op.create_table( - "knowledge_relations", - sa.Column( - "id", - postgresql.UUID(as_uuid=True), - primary_key=True, - nullable=False, - ), - sa.Column( - "source_entity_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("knowledge_entities.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column( - "target_entity_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("knowledge_entities.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("relation_type", sa.Enum(relation_type_enum, name="relationtype"), nullable=False), - sa.Column("properties", postgresql.JSONB(astext_type=sa.Text()), nullable=True, server_default="{}"), - sa.Column( - "source_chunk_id", - postgresql.UUID(as_uuid=True), - sa.ForeignKey("knowledge_chunks.id", ondelete="SET NULL"), - nullable=True, - ), - sa.Column("confidence", sa.String(20), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - ) - op.create_index("ix_relations_source", "knowledge_relations", ["source_entity_id"]) - op.create_index("ix_relations_target", "knowledge_relations", ["target_entity_id"]) - op.create_index("ix_relations_type", "knowledge_relations", ["relation_type"]) - - -def downgrade() -> None: - # 删除表(注意外键约束会自动处理) - op.drop_table("knowledge_relations") - op.drop_table("knowledge_entities") - # 删除枚举类型 - op.execute("DROP TYPE IF EXISTS relationtype") - op.execute("DROP TYPE IF EXISTS entitytype") diff --git a/backend/alembic/versions/g1h2i3j4kl56_add_missing_tables.py b/backend/alembic/versions/g1h2i3j4kl56_add_missing_tables.py deleted file mode 100644 index 3288721..0000000 --- a/backend/alembic/versions/g1h2i3j4kl56_add_missing_tables.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Add missing tables: brands, competitors, api_keys, usage_records, platform_rule_versions, detection_tasks - -Revision ID: g1h2i3j4kl56 -Revises: f7a8b9c0de56 -Create Date: 2026-05-26 10:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -revision: str = "g1h2i3j4kl56" -down_revision: Union[str, Sequence[str], None] = "f7a8b9c0de56" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.create_table( - "brands", - sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False), - sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False), - sa.Column("name", sa.String(50), nullable=False), - sa.Column("aliases", postgresql.JSONB(), server_default="[]", nullable=False), - sa.Column("website", sa.String(500), nullable=True), - sa.Column("industry", sa.String(50), nullable=True), - sa.Column("platforms", postgresql.JSONB(), server_default="[]", nullable=False), - sa.Column("frequency", sa.String(20), server_default="weekly", nullable=False), - sa.Column("status", sa.String(20), server_default="active", nullable=False), - sa.Column("last_queried_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("next_query_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("idx_brands_user_id", "brands", ["user_id"]) - - op.create_table( - "competitors", - sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False), - sa.Column("brand_id", postgresql.UUID(as_uuid=True), nullable=False), - sa.Column("name", sa.String(50), nullable=False), - sa.Column("aliases", postgresql.JSONB(), server_default="[]", nullable=False), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["brand_id"], ["brands.id"], ondelete="CASCADE"), - ) - op.create_index("idx_competitors_brand_id", "competitors", ["brand_id"]) - - op.create_table( - "api_keys", - sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False), - sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False), - sa.Column("engine_type", sa.String(20), nullable=False), - sa.Column("encrypted_key", sa.String(500), nullable=False), - sa.Column("key_hint", sa.String(50), nullable=False), - sa.Column("key_source", sa.String(10), server_default="user", nullable=True), - sa.Column("status", sa.String(20), server_default="active", nullable=True), - sa.Column("priority", sa.Integer(), server_default="0", nullable=True), - sa.Column("last_verified_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("idx_api_keys_user_id", "api_keys", ["user_id"]) - op.create_index("idx_api_keys_user_engine", "api_keys", ["user_id", "engine_type"]) - op.create_index("idx_api_keys_engine_status", "api_keys", ["engine_type", "status"]) - - op.create_table( - "usage_records", - sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False), - sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False), - sa.Column("brand_id", postgresql.UUID(as_uuid=True), nullable=True), - sa.Column("engine_type", sa.String(20), nullable=False), - sa.Column("query", sa.String(500), nullable=False), - sa.Column("input_tokens", sa.Integer(), server_default="0", nullable=True), - sa.Column("output_tokens", sa.Integer(), server_default="0", nullable=True), - sa.Column("cost", sa.Float(), server_default="0.0", nullable=True), - sa.Column("extra_data", postgresql.JSONB(), server_default="{}", nullable=True), - sa.Column("timestamp", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["brand_id"], ["brands.id"], ondelete="SET NULL"), - ) - op.create_index("idx_usage_records_user_id", "usage_records", ["user_id"]) - op.create_index("idx_usage_records_timestamp", "usage_records", ["timestamp"]) - op.create_index("idx_usage_records_user_engine", "usage_records", ["user_id", "engine_type"]) - op.create_index("idx_usage_records_user_timestamp", "usage_records", ["user_id", "timestamp"]) - op.create_index("idx_usage_records_engine_timestamp", "usage_records", ["engine_type", "timestamp"]) - - op.create_table( - "platform_rule_versions", - sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False), - sa.Column("rule_id", sa.String(100), nullable=False), - sa.Column("platform", sa.String(50), nullable=False), - sa.Column("version", sa.Integer(), nullable=False), - sa.Column("rule_data", postgresql.JSONB(), nullable=False), - sa.Column("change_summary", sa.String(500), nullable=True), - sa.Column("created_by", sa.String(100), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("idx_rule_versions_rule_id", "platform_rule_versions", ["rule_id"]) - op.create_index("idx_rule_versions_platform", "platform_rule_versions", ["platform"]) - - op.create_table( - "detection_tasks", - sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False), - sa.Column("brand_id", postgresql.UUID(as_uuid=True), nullable=False), - sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False), - sa.Column("name", sa.String(200), nullable=False), - sa.Column("frequency", sa.String(20), nullable=False), - sa.Column("engines", postgresql.JSONB(), server_default="[]", nullable=False), - sa.Column("queries", postgresql.JSONB(), server_default="[]", nullable=False), - sa.Column("competitor_names", postgresql.JSONB(), nullable=True), - sa.Column("is_active", sa.Boolean(), server_default="true", nullable=False), - sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("next_run_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), - sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["brand_id"], ["brands.id"], ondelete="CASCADE"), - ) - op.create_index("idx_detection_tasks_brand_id", "detection_tasks", ["brand_id"]) - op.create_index("idx_detection_tasks_user_id", "detection_tasks", ["user_id"]) - op.create_index("idx_detection_tasks_is_active", "detection_tasks", ["is_active"]) - - -def downgrade() -> None: - op.drop_index("idx_detection_tasks_is_active", table_name="detection_tasks") - op.drop_index("idx_detection_tasks_user_id", table_name="detection_tasks") - op.drop_index("idx_detection_tasks_brand_id", table_name="detection_tasks") - op.drop_table("detection_tasks") - - op.drop_index("idx_rule_versions_platform", table_name="platform_rule_versions") - op.drop_index("idx_rule_versions_rule_id", table_name="platform_rule_versions") - op.drop_table("platform_rule_versions") - - op.drop_index("idx_usage_records_engine_timestamp", table_name="usage_records") - op.drop_index("idx_usage_records_user_timestamp", table_name="usage_records") - op.drop_index("idx_usage_records_user_engine", table_name="usage_records") - op.drop_index("idx_usage_records_timestamp", table_name="usage_records") - op.drop_index("idx_usage_records_user_id", table_name="usage_records") - op.drop_table("usage_records") - - op.drop_index("idx_api_keys_engine_status", table_name="api_keys") - op.drop_index("idx_api_keys_user_engine", table_name="api_keys") - op.drop_index("idx_api_keys_user_id", table_name="api_keys") - op.drop_table("api_keys") - - op.drop_index("idx_competitors_brand_id", table_name="competitors") - op.drop_table("competitors") - - op.drop_index("idx_brands_user_id", table_name="brands") - op.drop_table("brands") diff --git a/backend/alembic/versions/h3i4j5k6mn78_add_timezone_to_all_datetime_columns.py b/backend/alembic/versions/h3i4j5k6mn78_add_timezone_to_all_datetime_columns.py deleted file mode 100644 index c03d29f..0000000 --- a/backend/alembic/versions/h3i4j5k6mn78_add_timezone_to_all_datetime_columns.py +++ /dev/null @@ -1,278 +0,0 @@ -"""Add timezone to all datetime columns - -Revision ID: h3i4j5k6mn78 -Revises: f063b3da67b6 -Create Date: 2026-06-01 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - -revision: str = 'h3i4j5k6mn78' -down_revision: Union[str, Sequence[str], None] = 'f063b3da67b6' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.alter_column('users', 'lastLoginAt', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='lastLoginAt AT TIME ZONE \'UTC\'') - op.alter_column('users', 'createdAt', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='createdAt AT TIME ZONE \'UTC\'') - op.alter_column('users', 'updatedAt', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updatedAt AT TIME ZONE \'UTC\'') - op.alter_column('users', 'lockedUntil', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='lockedUntil AT TIME ZONE \'UTC\'') - - op.alter_column('brands', 'last_queried_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='last_queried_at AT TIME ZONE \'UTC\'') - op.alter_column('brands', 'next_query_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='next_query_at AT TIME ZONE \'UTC\'') - op.alter_column('brands', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('brands', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('queries', 'last_queried_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='last_queried_at AT TIME ZONE \'UTC\'') - op.alter_column('queries', 'next_query_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='next_query_at AT TIME ZONE \'UTC\'') - op.alter_column('queries', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('queries', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('citation_records', 'queried_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='queried_at AT TIME ZONE \'UTC\'') - - op.alter_column('attribution_records', 'published_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='published_at AT TIME ZONE \'UTC\'') - op.alter_column('attribution_records', 'window_end_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='window_end_at AT TIME ZONE \'UTC\'') - op.alter_column('attribution_records', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('attribution_records', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('contents', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('contents', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('content_versions', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('content_reviews', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('geo_plans', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('geo_plans', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('geo_plan_actions', 'completed_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='completed_at AT TIME ZONE \'UTC\'') - op.alter_column('geo_plan_actions', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('geo_plan_actions', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('suggestions', 'generated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='generated_at AT TIME ZONE \'UTC\'') - op.alter_column('suggestions', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('competitors', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('competitor_insights', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('competitor_insights', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('distribution_schedules', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('distribution_schedules', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('brand_knowledge', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('brand_knowledge', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('keywords', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('agent_registry', 'last_heartbeat', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='last_heartbeat AT TIME ZONE \'UTC\'') - op.alter_column('agent_registry', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('agent_registry', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('agent_configs', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('agent_tasks', 'scheduled_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='scheduled_at AT TIME ZONE \'UTC\'') - op.alter_column('agent_tasks', 'started_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='started_at AT TIME ZONE \'UTC\'') - op.alter_column('agent_tasks', 'completed_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='completed_at AT TIME ZONE \'UTC\'') - op.alter_column('agent_tasks', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('agent_task_logs', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('detection_tasks', 'last_run_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='last_run_at AT TIME ZONE \'UTC\'') - op.alter_column('detection_tasks', 'next_run_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='next_run_at AT TIME ZONE \'UTC\'') - op.alter_column('detection_tasks', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('detection_tasks', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('monitoring_records', 'last_checked_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='last_checked_at AT TIME ZONE \'UTC\'') - op.alter_column('monitoring_records', 'next_check_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='next_check_at AT TIME ZONE \'UTC\'') - op.alter_column('monitoring_records', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('monitoring_records', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('content_baselines', 'recorded_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='recorded_at AT TIME ZONE \'UTC\'') - - op.alter_column('trend_insights', 'period_start', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='period_start AT TIME ZONE \'UTC\'') - op.alter_column('trend_insights', 'period_end', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='period_end AT TIME ZONE \'UTC\'') - op.alter_column('trend_insights', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('trend_insights', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('query_tasks', 'scheduled_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='scheduled_at AT TIME ZONE \'UTC\'') - op.alter_column('query_tasks', 'started_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='started_at AT TIME ZONE \'UTC\'') - op.alter_column('query_tasks', 'completed_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='completed_at AT TIME ZONE \'UTC\'') - - op.alter_column('usage_records', 'timestamp', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='timestamp AT TIME ZONE \'UTC\'') - op.alter_column('usage_records', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('api_keys', 'last_verified_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='last_verified_at AT TIME ZONE \'UTC\'') - op.alter_column('api_keys', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('api_keys', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('knowledge_bases', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('knowledge_bases', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('knowledge_documents', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('knowledge_documents', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('knowledge_chunks', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('knowledge_search_logs', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('knowledge_entities', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('knowledge_entities', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('knowledge_relations', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('organizations', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('organizations', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('org_members', 'joined_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='joined_at AT TIME ZONE \'UTC\'') - - op.alter_column('lifecycle_projects', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('lifecycle_projects', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('project_stages', 'started_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='started_at AT TIME ZONE \'UTC\'') - op.alter_column('project_stages', 'completed_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='completed_at AT TIME ZONE \'UTC\'') - - op.alter_column('alerts', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('alert_settings', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('alert_settings', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('platform_rules', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('platform_rule_versions', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('schema_suggestions', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('schema_suggestions', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - - op.alter_column('subscriptions', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - op.alter_column('payment_orders', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('payment_orders', 'updated_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='updated_at AT TIME ZONE \'UTC\'') - op.alter_column('payment_orders', 'paid_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='paid_at AT TIME ZONE \'UTC\'') - - op.alter_column('diagnosis_records', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('diagnosis_records', 'completed_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='completed_at AT TIME ZONE \'UTC\'') - - op.alter_column('publish_records', 'published_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='published_at AT TIME ZONE \'UTC\'') - op.alter_column('publish_records', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - op.alter_column('content_metrics', 'recorded_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='recorded_at AT TIME ZONE \'UTC\'') - op.alter_column('optimization_insights', 'created_at', type_=sa.DateTime(timezone=True), existing_type=sa.DateTime(), postgresql_using='created_at AT TIME ZONE \'UTC\'') - - -def downgrade() -> None: - op.alter_column('users', 'lastLoginAt', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('users', 'createdAt', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('users', 'updatedAt', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('users', 'lockedUntil', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('brands', 'last_queried_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('brands', 'next_query_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('brands', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('brands', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('queries', 'last_queried_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('queries', 'next_query_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('queries', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('queries', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('citation_records', 'queried_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('attribution_records', 'published_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('attribution_records', 'window_end_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('attribution_records', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('attribution_records', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('contents', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('contents', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('content_versions', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('content_reviews', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('geo_plans', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('geo_plans', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('geo_plan_actions', 'completed_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('geo_plan_actions', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('geo_plan_actions', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('suggestions', 'generated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('suggestions', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('competitors', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('competitor_insights', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('competitor_insights', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('distribution_schedules', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('distribution_schedules', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('brand_knowledge', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('brand_knowledge', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('keywords', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('agent_registry', 'last_heartbeat', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('agent_registry', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('agent_registry', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('agent_configs', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('agent_tasks', 'scheduled_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('agent_tasks', 'started_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('agent_tasks', 'completed_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('agent_tasks', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('agent_task_logs', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('detection_tasks', 'last_run_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('detection_tasks', 'next_run_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('detection_tasks', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('detection_tasks', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('monitoring_records', 'last_checked_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('monitoring_records', 'next_check_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('monitoring_records', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('monitoring_records', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('content_baselines', 'recorded_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('trend_insights', 'period_start', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('trend_insights', 'period_end', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('trend_insights', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('trend_insights', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('query_tasks', 'scheduled_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('query_tasks', 'started_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('query_tasks', 'completed_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('usage_records', 'timestamp', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('usage_records', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('api_keys', 'last_verified_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('api_keys', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('api_keys', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('knowledge_bases', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('knowledge_bases', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('knowledge_documents', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('knowledge_documents', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('knowledge_chunks', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('knowledge_search_logs', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('knowledge_entities', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('knowledge_entities', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('knowledge_relations', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('organizations', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('organizations', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('org_members', 'joined_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('lifecycle_projects', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('lifecycle_projects', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('project_stages', 'started_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('project_stages', 'completed_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('alerts', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('alert_settings', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('alert_settings', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('platform_rules', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('platform_rule_versions', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('schema_suggestions', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('schema_suggestions', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('subscriptions', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('payment_orders', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('payment_orders', 'updated_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('payment_orders', 'paid_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('diagnosis_records', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('diagnosis_records', 'completed_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - - op.alter_column('publish_records', 'published_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('publish_records', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('content_metrics', 'recorded_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True)) - op.alter_column('optimization_insights', 'created_at', type_=sa.DateTime(), existing_type=sa.DateTime(timezone=True))