You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
176 lines
9.9 KiB
176 lines
9.9 KiB
|
2 months ago
|
# 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()
|