fix(models): add alembic migration 009 for missing tables (v1.4.1)

Migration adds tables that existed in models/ but were never
included in alembic history:
- ai_ops: device_health_scores, anomaly_detections, diagnostic_reports,
  maintenance_predictions, ops_insights
- energy_strategy: tou_pricing, tou_pricing_periods, energy_strategies,
  strategy_executions, monthly_cost_reports
- weather: weather_data, weather_config
- prediction: prediction_tasks, prediction_results, optimization_schedules

Without this migration, fresh deploys would 500 on these endpoints:
- /api/v1/ai-ops/health, /ai-ops/dashboard
- /api/v1/strategy/pricing
- /api/v1/prediction/forecast
- /api/v1/weather/current

Discovered during Z-Park demo deployment on xie_openclaw1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-10 09:47:34 +08:00
parent 56132bae32
commit 72f4269cd4
7 changed files with 519 additions and 4 deletions

View File

@@ -0,0 +1,287 @@
"""Add ai_ops, energy_strategy, weather, prediction tables
Revision ID: 009_aiops_strategy
Revises: 008_management
Create Date: 2026-04-10
Adds tables for features that were previously missing from alembic history:
- AI Ops: device_health_scores, anomaly_detections, diagnostic_reports,
maintenance_predictions, ops_insights
- Energy Strategy: tou_pricing, tou_pricing_periods, energy_strategies,
strategy_executions, monthly_cost_reports
- Weather: weather_data, weather_config
- Prediction: prediction_tasks, prediction_results, optimization_schedules
"""
from alembic import op
import sqlalchemy as sa
revision = "009_aiops_strategy"
down_revision = "008_management"
branch_labels = None
depends_on = None
def upgrade() -> None:
# =========================================================================
# AI Ops tables
# =========================================================================
op.create_table(
"device_health_scores",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("health_score", sa.Float, nullable=False),
sa.Column("status", sa.String(20), default="healthy"),
sa.Column("factors", sa.JSON),
sa.Column("trend", sa.String(20), default="stable"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_device_health_scores_device_id", "device_health_scores", ["device_id"])
op.create_index("ix_device_health_scores_timestamp", "device_health_scores", ["timestamp"])
op.create_table(
"anomaly_detections",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
sa.Column("detected_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("anomaly_type", sa.String(50), nullable=False),
sa.Column("severity", sa.String(20), default="warning"),
sa.Column("description", sa.Text),
sa.Column("metric_name", sa.String(50)),
sa.Column("expected_value", sa.Float),
sa.Column("actual_value", sa.Float),
sa.Column("deviation_percent", sa.Float),
sa.Column("status", sa.String(20), default="detected"),
sa.Column("resolution_notes", sa.Text),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_anomaly_detections_device_id", "anomaly_detections", ["device_id"])
op.create_index("ix_anomaly_detections_detected_at", "anomaly_detections", ["detected_at"])
op.create_table(
"diagnostic_reports",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
sa.Column("generated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("report_type", sa.String(20), default="routine"),
sa.Column("findings", sa.JSON),
sa.Column("recommendations", sa.JSON),
sa.Column("estimated_impact", sa.JSON),
sa.Column("status", sa.String(20), default="generated"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_diagnostic_reports_device_id", "diagnostic_reports", ["device_id"])
op.create_table(
"maintenance_predictions",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
sa.Column("predicted_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("component", sa.String(100)),
sa.Column("failure_mode", sa.String(200)),
sa.Column("probability", sa.Float),
sa.Column("predicted_failure_date", sa.DateTime(timezone=True)),
sa.Column("recommended_action", sa.Text),
sa.Column("urgency", sa.String(20), default="medium"),
sa.Column("estimated_downtime_hours", sa.Float),
sa.Column("estimated_repair_cost", sa.Float),
sa.Column("status", sa.String(20), default="predicted"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_maintenance_predictions_device_id", "maintenance_predictions", ["device_id"])
op.create_table(
"ops_insights",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("insight_type", sa.String(50), nullable=False),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("description", sa.Text),
sa.Column("data", sa.JSON),
sa.Column("impact_level", sa.String(20), default="medium"),
sa.Column("actionable", sa.Boolean, default=False),
sa.Column("recommended_action", sa.Text),
sa.Column("generated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("valid_until", sa.DateTime(timezone=True)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# =========================================================================
# Energy Strategy tables
# =========================================================================
op.create_table(
"tou_pricing",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("region", sa.String(100), default="北京"),
sa.Column("effective_date", sa.Date),
sa.Column("end_date", sa.Date),
sa.Column("is_active", sa.Boolean, default=True),
sa.Column("created_by", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"tou_pricing_periods",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("pricing_id", sa.Integer, sa.ForeignKey("tou_pricing.id", ondelete="CASCADE"), nullable=False),
sa.Column("period_type", sa.String(20), nullable=False),
sa.Column("start_time", sa.String(10), nullable=False),
sa.Column("end_time", sa.String(10), nullable=False),
sa.Column("price_yuan_per_kwh", sa.Float, nullable=False),
sa.Column("month_range", sa.String(50)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"energy_strategies",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("strategy_type", sa.String(50), nullable=False),
sa.Column("description", sa.String(500)),
sa.Column("parameters", sa.JSON),
sa.Column("is_enabled", sa.Boolean, default=False),
sa.Column("priority", sa.Integer, default=0),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"strategy_executions",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("strategy_id", sa.Integer, sa.ForeignKey("energy_strategies.id"), nullable=False),
sa.Column("date", sa.Date, nullable=False),
sa.Column("actions_taken", sa.JSON),
sa.Column("savings_kwh", sa.Float, default=0),
sa.Column("savings_yuan", sa.Float, default=0),
sa.Column("status", sa.String(20), default="planned"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"monthly_cost_reports",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("year_month", sa.String(7), nullable=False, unique=True),
sa.Column("total_consumption_kwh", sa.Float, default=0),
sa.Column("total_cost_yuan", sa.Float, default=0),
sa.Column("peak_consumption", sa.Float, default=0),
sa.Column("valley_consumption", sa.Float, default=0),
sa.Column("flat_consumption", sa.Float, default=0),
sa.Column("sharp_peak_consumption", sa.Float, default=0),
sa.Column("pv_self_consumption", sa.Float, default=0),
sa.Column("pv_feed_in", sa.Float, default=0),
sa.Column("optimized_cost", sa.Float, default=0),
sa.Column("baseline_cost", sa.Float, default=0),
sa.Column("savings_yuan", sa.Float, default=0),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# =========================================================================
# Weather tables
# =========================================================================
op.create_table(
"weather_data",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
sa.Column("data_type", sa.String(20), nullable=False),
sa.Column("temperature", sa.Float),
sa.Column("humidity", sa.Float),
sa.Column("solar_radiation", sa.Float),
sa.Column("cloud_cover", sa.Float),
sa.Column("wind_speed", sa.Float),
sa.Column("source", sa.String(20), default="mock"),
sa.Column("fetched_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_weather_data_timestamp", "weather_data", ["timestamp"])
op.create_table(
"weather_config",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("api_provider", sa.String(50), default="mock"),
sa.Column("api_key", sa.String(200)),
sa.Column("location_lat", sa.Float, default=39.9),
sa.Column("location_lon", sa.Float, default=116.4),
sa.Column("fetch_interval_minutes", sa.Integer, default=30),
sa.Column("is_enabled", sa.Boolean, default=True),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# =========================================================================
# Prediction tables
# =========================================================================
op.create_table(
"prediction_tasks",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id")),
sa.Column("prediction_type", sa.String(50), nullable=False),
sa.Column("horizon_hours", sa.Integer, default=24),
sa.Column("status", sa.String(20), default="pending"),
sa.Column("parameters", sa.JSON),
sa.Column("error_message", sa.Text),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("completed_at", sa.DateTime(timezone=True)),
)
op.create_table(
"prediction_results",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("task_id", sa.Integer, sa.ForeignKey("prediction_tasks.id"), nullable=False),
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
sa.Column("predicted_value", sa.Float, nullable=False),
sa.Column("confidence_lower", sa.Float),
sa.Column("confidence_upper", sa.Float),
sa.Column("actual_value", sa.Float),
sa.Column("unit", sa.String(20)),
)
op.create_index("ix_prediction_results_task_id", "prediction_results", ["task_id"])
op.create_index("ix_prediction_results_timestamp", "prediction_results", ["timestamp"])
op.create_table(
"optimization_schedules",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id")),
sa.Column("date", sa.DateTime(timezone=True), nullable=False),
sa.Column("schedule_data", sa.JSON),
sa.Column("expected_savings_kwh", sa.Float, default=0),
sa.Column("expected_savings_yuan", sa.Float, default=0),
sa.Column("status", sa.String(20), default="pending"),
sa.Column("approved_by", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("approved_at", sa.DateTime(timezone=True)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_optimization_schedules_date", "optimization_schedules", ["date"])
def downgrade() -> None:
# Prediction
op.drop_index("ix_optimization_schedules_date", table_name="optimization_schedules")
op.drop_table("optimization_schedules")
op.drop_index("ix_prediction_results_timestamp", table_name="prediction_results")
op.drop_index("ix_prediction_results_task_id", table_name="prediction_results")
op.drop_table("prediction_results")
op.drop_table("prediction_tasks")
# Weather
op.drop_table("weather_config")
op.drop_index("ix_weather_data_timestamp", table_name="weather_data")
op.drop_table("weather_data")
# Energy Strategy
op.drop_table("monthly_cost_reports")
op.drop_table("strategy_executions")
op.drop_table("energy_strategies")
op.drop_table("tou_pricing_periods")
op.drop_table("tou_pricing")
# AI Ops
op.drop_table("ops_insights")
op.drop_index("ix_maintenance_predictions_device_id", table_name="maintenance_predictions")
op.drop_table("maintenance_predictions")
op.drop_index("ix_diagnostic_reports_device_id", table_name="diagnostic_reports")
op.drop_table("diagnostic_reports")
op.drop_index("ix_anomaly_detections_detected_at", table_name="anomaly_detections")
op.drop_index("ix_anomaly_detections_device_id", table_name="anomaly_detections")
op.drop_table("anomaly_detections")
op.drop_index("ix_device_health_scores_timestamp", table_name="device_health_scores")
op.drop_index("ix_device_health_scores_device_id", table_name="device_health_scores")
op.drop_table("device_health_scores")