# backend/app/db/migrations_v2_1.py """ v2.1 数据库迁移脚本 创建告警、订阅、质量监控相关表 """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB # revision identifiers revision = 'v2.1.0' down_revision = 'v2.0.0' branch_labels = None depends_on = None def upgrade(): """升级数据库""" # 1. 告警规则表 op.create_table( 'alert_rule', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('name', sa.String(100), nullable=False, comment='告警名称'), sa.Column('symbol', sa.String(20), nullable=False, comment='品种代码'), sa.Column('type', sa.String(20), nullable=False, comment='告警类型: price/change_percent/technical/volume'), sa.Column('condition', JSONB(), nullable=False, comment='触发条件: {"field": "price", "operator": "gt", "value": 3900}'), sa.Column('channels', JSONB(), nullable=False, comment='通知渠道: ["站内消息", "邮件"]'), sa.Column('enabled', sa.Boolean(), default=True, comment='是否启用'), sa.Column('start_time', sa.Time(), nullable=True, comment='生效开始时间'), sa.Column('end_time', sa.Time(), nullable=True, comment='生效结束时间'), sa.Column('repeat_interval', sa.Integer(), default=0, comment='重复间隔(秒), 0表示仅触发一次'), sa.Column('last_triggered_at', sa.TIMESTAMP(timezone=True), nullable=True, comment='上次触发时间'), sa.Column('trigger_count', sa.Integer(), default=0, comment='触发次数'), sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now()), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now()), comment='告警规则表' ) # 创建索引 op.create_index('idx_alert_rule_user', 'alert_rule', ['user_id']) op.create_index('idx_alert_rule_symbol', 'alert_rule', ['symbol']) op.create_index('idx_alert_rule_enabled', 'alert_rule', ['enabled']) op.create_index('idx_alert_rule_type', 'alert_rule', ['type']) # 2. 告警历史表 op.create_table( 'alert_history', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('rule_id', sa.Integer(), sa.ForeignKey('alert_rule.id', ondelete='CASCADE'), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('symbol', sa.String(20), nullable=False), sa.Column('trigger_value', sa.Numeric(20, 4), nullable=True, comment='触发时的值'), sa.Column('trigger_condition', sa.Text(), nullable=True, comment='触发条件描述'), sa.Column('notified', sa.Boolean(), default=False, comment='是否已发送通知'), sa.Column('notify_channels', JSONB(), nullable=True, comment='已发送的通知渠道'), sa.Column('notify_time', sa.TIMESTAMP(timezone=True), nullable=True, comment='通知发送时间'), sa.Column('trigger_time', sa.TIMESTAMP(timezone=True), server_default=sa.func.now(), comment='触发时间'), sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now()), comment='告警历史表' ) op.create_index('idx_alert_history_rule', 'alert_history', ['rule_id']) op.create_index('idx_alert_history_user', 'alert_history', ['user_id']) op.create_index('idx_alert_history_time', 'alert_history', ['trigger_time']) op.create_index('idx_alert_history_symbol', 'alert_history', ['symbol']) # 3. 数据订阅表 op.create_table( 'subscription', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('name', sa.String(100), nullable=True, comment='订阅名称'), sa.Column('topics', JSONB(), nullable=False, comment='订阅主题: ["kline.update.IF2406.1m"]'), sa.Column('callback_url', sa.String(500), nullable=True, comment='回调地址'), sa.Column('callback_method', sa.String(10), default='POST', comment='回调方法'), sa.Column('callback_headers', JSONB(), nullable=True, comment='回调请求头'), sa.Column('enabled', sa.Boolean(), default=True, comment='是否启用'), sa.Column('status', sa.String(20), default='active', comment='状态: active/paused/error'), sa.Column('last_callback_time', sa.TIMESTAMP(timezone=True), nullable=True, comment='上次回调时间'), sa.Column('last_callback_status', sa.String(20), nullable=True, comment='上次回调状态'), sa.Column('callback_count', sa.Integer(), default=0, comment='回调次数'), sa.Column('error_count', sa.Integer(), default=0, comment='错误次数'), sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now()), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now()), comment='数据订阅表' ) op.create_index('idx_subscription_user', 'subscription', ['user_id']) op.create_index('idx_subscription_status', 'subscription', ['status']) op.create_index('idx_subscription_enabled', 'subscription', ['enabled']) # 4. 数据质量规则表 op.create_table( 'quality_rule', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('name', sa.String(100), nullable=False, comment='规则名称'), sa.Column('symbol', sa.String(20), nullable=True, comment='品种代码, 为空表示全局规则'), sa.Column('metric', sa.String(50), nullable=False, comment='监控指标: completeness/accuracy/timeliness/consistency'), sa.Column('condition', sa.Text(), nullable=False, comment='条件表达式'), sa.Column('threshold', sa.Numeric(5, 2), nullable=False, comment='阈值'), sa.Column('level', sa.String(20), default='warning', comment='告警级别: info/warning/critical'), sa.Column('enabled', sa.Boolean(), default=True, comment='是否启用'), sa.Column('description', sa.Text(), nullable=True, comment='规则描述'), sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now()), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now()), comment='数据质量规则表' ) op.create_index('idx_quality_rule_metric', 'quality_rule', ['metric']) op.create_index('idx_quality_rule_enabled', 'quality_rule', ['enabled']) op.create_index('idx_quality_rule_symbol', 'quality_rule', ['symbol']) # 5. 数据质量日志表 op.create_table( 'quality_log', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('rule_id', sa.Integer(), sa.ForeignKey('quality_rule.id', ondelete='SET NULL'), nullable=True), sa.Column('symbol', sa.String(20), nullable=True, comment='品种代码'), sa.Column('metric', sa.String(50), nullable=False, comment='监控指标'), sa.Column('metric_value', sa.Numeric(10, 4), nullable=True, comment='指标值'), sa.Column('threshold', sa.Numeric(5, 2), nullable=True, comment='阈值'), sa.Column('level', sa.String(20), nullable=False, comment='告警级别'), sa.Column('triggered', sa.Boolean(), default=False, comment='是否触发告警'), sa.Column('message', sa.Text(), nullable=True, comment='详细信息'), sa.Column('details', JSONB(), nullable=True, comment='详细信息'), sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now()), comment='数据质量日志表' ) op.create_index('idx_quality_log_rule', 'quality_log', ['rule_id']) op.create_index('idx_quality_log_time', 'quality_log', ['created_at']) op.create_index('idx_quality_log_symbol', 'quality_log', ['symbol']) op.create_index('idx_quality_log_metric', 'quality_log', ['metric']) # 6. WebSocket 连接表(用于统计和监控) op.create_table( 'websocket_connection', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('connection_id', sa.String(100), nullable=False, unique=True, comment='连接ID'), sa.Column('client_ip', sa.String(50), nullable=True, comment='客户端IP'), sa.Column('user_agent', sa.String(500), nullable=True, comment='用户代理'), sa.Column('connected_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now(), comment='连接时间'), sa.Column('disconnected_at', sa.TIMESTAMP(timezone=True), nullable=True, comment='断开时间'), sa.Column('last_heartbeat', sa.TIMESTAMP(timezone=True), server_default=sa.func.now(), comment='最后心跳'), sa.Column('subscriptions', JSONB(), nullable=True, comment='订阅列表'), sa.Column('message_count', sa.Integer(), default=0, comment='消息数量'), sa.Column('status', sa.String(20), default='connected', comment='状态: connected/disconnected'), comment='WebSocket连接表' ) op.create_index('idx_ws_connection_user', 'websocket_connection', ['user_id']) op.create_index('idx_ws_connection_status', 'websocket_connection', ['status']) op.create_index('idx_ws_connection_connected', 'websocket_connection', ['connected_at']) print("✅ v2.1 数据库迁移完成") print("创建表: alert_rule, alert_history, subscription, quality_rule, quality_log, websocket_connection") def downgrade(): """回滚数据库""" op.drop_table('quality_log') op.drop_table('quality_rule') op.drop_table('subscription') op.drop_table('alert_history') op.drop_table('alert_rule') op.drop_table('websocket_connection') print("✅ v2.1 数据库回滚完成") if __name__ == "__main__": # 直接运行时执行迁移 upgrade()