ems-core v1.0.0: Standard EMS platform core

Shared backend + frontend for multi-customer EMS deployments.
- 12 enterprise modules: quota, cost, charging, maintenance, analysis, etc.
- 120+ API endpoints, 37 database tables
- Customer config mechanism (CUSTOMER env var + YAML config)
- Collectors: Modbus TCP, MQTT, HTTP API, Sungrow iSolarCloud
- Frontend: React 19 + Ant Design + ECharts + Three.js
- Infrastructure: Redis cache, rate limiting, aggregation engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-04 18:14:11 +08:00
commit 92ec910a13
227 changed files with 39179 additions and 0 deletions

49
backend/alembic/env.py Normal file
View File

@@ -0,0 +1,49 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from app.core.config import get_settings
from app.core.database import Base
from app.models import * # noqa
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Override sqlalchemy.url from app settings (supports .env override)
app_settings = get_settings()
config.set_main_option("sqlalchemy.url", app_settings.DATABASE_URL_SYNC)
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,268 @@
"""Initial schema - all 14 tables
Revision ID: 001_initial
Revises:
Create Date: 2026-04-01
"""
from alembic import op
import sqlalchemy as sa
revision = "001_initial"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- roles ---
op.create_table(
"roles",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(50), unique=True, nullable=False),
sa.Column("display_name", sa.String(100), nullable=False),
sa.Column("description", sa.Text),
sa.Column("permissions", sa.Text),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- users ---
op.create_table(
"users",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("username", sa.String(50), unique=True, nullable=False),
sa.Column("email", sa.String(100), unique=True),
sa.Column("hashed_password", sa.String(200), nullable=False),
sa.Column("full_name", sa.String(100)),
sa.Column("phone", sa.String(20)),
sa.Column("role", sa.String(50), sa.ForeignKey("roles.name"), default="visitor"),
sa.Column("is_active", sa.Boolean, default=True),
sa.Column("last_login", sa.DateTime(timezone=True)),
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_index("ix_users_username", "users", ["username"])
# --- audit_logs ---
op.create_table(
"audit_logs",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("action", sa.String(50), nullable=False),
sa.Column("resource", sa.String(100)),
sa.Column("detail", sa.Text),
sa.Column("ip_address", sa.String(50)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- device_types ---
op.create_table(
"device_types",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("code", sa.String(50), unique=True, nullable=False),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("icon", sa.String(100)),
sa.Column("data_fields", sa.JSON),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- device_groups ---
op.create_table(
"device_groups",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("parent_id", sa.Integer, sa.ForeignKey("device_groups.id")),
sa.Column("location", sa.String(200)),
sa.Column("description", sa.Text),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- devices ---
op.create_table(
"devices",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("code", sa.String(100), unique=True, nullable=False),
sa.Column("device_type", sa.String(50), sa.ForeignKey("device_types.code"), nullable=False),
sa.Column("group_id", sa.Integer, sa.ForeignKey("device_groups.id")),
sa.Column("model", sa.String(100)),
sa.Column("manufacturer", sa.String(100)),
sa.Column("serial_number", sa.String(100)),
sa.Column("rated_power", sa.Float),
sa.Column("install_date", sa.DateTime(timezone=True)),
sa.Column("location", sa.String(200)),
sa.Column("protocol", sa.String(50)),
sa.Column("connection_params", sa.JSON),
sa.Column("collect_interval", sa.Integer, default=15),
sa.Column("status", sa.String(20), default="offline"),
sa.Column("is_active", sa.Boolean, default=True),
sa.Column("metadata", sa.JSON),
sa.Column("last_data_time", sa.DateTime(timezone=True)),
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()),
)
# --- energy_data ---
op.create_table(
"energy_data",
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), nullable=False),
sa.Column("data_type", sa.String(50), nullable=False),
sa.Column("value", sa.Float, nullable=False),
sa.Column("unit", sa.String(20)),
sa.Column("quality", sa.Integer, default=0),
sa.Column("raw_data", sa.JSON),
)
op.create_index("ix_energy_data_device_id", "energy_data", ["device_id"])
op.create_index("ix_energy_data_timestamp", "energy_data", ["timestamp"])
# --- energy_daily_summary ---
op.create_table(
"energy_daily_summary",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
sa.Column("date", sa.DateTime(timezone=True), nullable=False),
sa.Column("energy_type", sa.String(50), nullable=False),
sa.Column("total_consumption", sa.Float, default=0),
sa.Column("total_generation", sa.Float, default=0),
sa.Column("peak_power", sa.Float),
sa.Column("min_power", sa.Float),
sa.Column("avg_power", sa.Float),
sa.Column("operating_hours", sa.Float),
sa.Column("avg_cop", sa.Float),
sa.Column("avg_temperature", sa.Float),
sa.Column("cost", sa.Float),
sa.Column("carbon_emission", sa.Float),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_energy_daily_summary_device_id", "energy_daily_summary", ["device_id"])
op.create_index("ix_energy_daily_summary_date", "energy_daily_summary", ["date"])
# --- alarm_rules ---
op.create_table(
"alarm_rules",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id")),
sa.Column("device_type", sa.String(50)),
sa.Column("data_type", sa.String(50), nullable=False),
sa.Column("condition", sa.String(20), nullable=False),
sa.Column("threshold", sa.Float),
sa.Column("threshold_high", sa.Float),
sa.Column("threshold_low", sa.Float),
sa.Column("duration", sa.Integer, default=0),
sa.Column("severity", sa.String(20), default="warning"),
sa.Column("notify_channels", sa.JSON),
sa.Column("notify_targets", sa.JSON),
sa.Column("auto_action", sa.JSON),
sa.Column("silence_start", sa.String(10)),
sa.Column("silence_end", sa.String(10)),
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()),
)
# --- alarm_events ---
op.create_table(
"alarm_events",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("rule_id", sa.Integer, sa.ForeignKey("alarm_rules.id")),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
sa.Column("severity", sa.String(20), nullable=False),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("description", sa.Text),
sa.Column("value", sa.Float),
sa.Column("threshold", sa.Float),
sa.Column("status", sa.String(20), default="active"),
sa.Column("acknowledged_by", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("acknowledged_at", sa.DateTime(timezone=True)),
sa.Column("resolved_at", sa.DateTime(timezone=True)),
sa.Column("resolve_note", sa.Text),
sa.Column("triggered_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- emission_factors ---
op.create_table(
"emission_factors",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("energy_type", sa.String(50), nullable=False),
sa.Column("factor", sa.Float, nullable=False),
sa.Column("unit", sa.String(20), nullable=False),
sa.Column("region", sa.String(50), default="north_china"),
sa.Column("scope", sa.Integer, nullable=False),
sa.Column("source", sa.String(200)),
sa.Column("year", sa.Integer),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- carbon_emissions ---
op.create_table(
"carbon_emissions",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("date", sa.DateTime(timezone=True), nullable=False),
sa.Column("scope", sa.Integer, nullable=False),
sa.Column("category", sa.String(50), nullable=False),
sa.Column("emission", sa.Float, nullable=False),
sa.Column("reduction", sa.Float, default=0),
sa.Column("energy_consumption", sa.Float),
sa.Column("energy_unit", sa.String(20)),
sa.Column("emission_factor_id", sa.Integer),
sa.Column("note", sa.Text),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_carbon_emissions_date", "carbon_emissions", ["date"])
# --- report_templates ---
op.create_table(
"report_templates",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("report_type", sa.String(50), nullable=False),
sa.Column("description", sa.Text),
sa.Column("fields", sa.JSON, nullable=False),
sa.Column("filters", sa.JSON),
sa.Column("aggregation", sa.String(20), default="sum"),
sa.Column("time_granularity", sa.String(20), default="hour"),
sa.Column("format_config", sa.JSON),
sa.Column("is_system", sa.Boolean, default=False),
sa.Column("created_by", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- report_tasks ---
op.create_table(
"report_tasks",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("template_id", sa.Integer, sa.ForeignKey("report_templates.id"), nullable=False),
sa.Column("name", sa.String(200)),
sa.Column("schedule", sa.String(50)),
sa.Column("next_run", sa.DateTime(timezone=True)),
sa.Column("last_run", sa.DateTime(timezone=True)),
sa.Column("recipients", sa.JSON),
sa.Column("export_format", sa.String(20), default="xlsx"),
sa.Column("file_path", sa.String(500)),
sa.Column("status", sa.String(20), default="pending"),
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()),
)
def downgrade() -> None:
op.drop_table("report_tasks")
op.drop_table("report_templates")
op.drop_table("carbon_emissions")
op.drop_table("emission_factors")
op.drop_table("alarm_events")
op.drop_table("alarm_rules")
op.drop_table("energy_daily_summary")
op.drop_table("energy_data")
op.drop_table("devices")
op.drop_table("device_groups")
op.drop_table("device_types")
op.drop_table("audit_logs")
op.drop_table("users")
op.drop_table("roles")

View File

@@ -0,0 +1,41 @@
"""Add system_settings table
Revision ID: 002_system_settings
Revises: 001_initial
Create Date: 2026-04-02
"""
from alembic import op
import sqlalchemy as sa
revision = "002_system_settings"
down_revision = "001_initial"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"system_settings",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("key", sa.String(100), unique=True, nullable=False, index=True),
sa.Column("value", sa.Text, nullable=False, server_default=""),
sa.Column("description", sa.String(255)),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# Seed default settings
op.execute("""
INSERT INTO system_settings (key, value, description) VALUES
('platform_name', '天普零碳园区智慧能源管理平台', '平台名称'),
('data_retention_days', '365', '数据保留天数'),
('alarm_auto_resolve_minutes', '30', '告警自动解除时间(分钟)'),
('simulator_interval_seconds', '15', '模拟器采集间隔(秒)'),
('notification_email_enabled', 'false', '是否启用邮件通知'),
('notification_email_smtp', '', 'SMTP服务器地址'),
('report_auto_schedule_enabled', 'false', '是否启用自动报表'),
('timezone', 'Asia/Shanghai', '系统时区')
""")
def downgrade() -> None:
op.drop_table("system_settings")

View File

@@ -0,0 +1,37 @@
"""Add energy_categories table and devices.category_id
Revision ID: 003_energy_categories
Revises: 002_system_settings
Create Date: 2026-04-03
"""
from alembic import op
import sqlalchemy as sa
revision = "003_energy_categories"
down_revision = "002_system_settings"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- energy_categories ---
op.create_table(
"energy_categories",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("code", sa.String(50), unique=True, nullable=False),
sa.Column("parent_id", sa.Integer, sa.ForeignKey("energy_categories.id")),
sa.Column("sort_order", sa.Integer, default=0),
sa.Column("icon", sa.String(100)),
sa.Column("color", sa.String(20)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# Add category_id column to devices table (batch mode for SQLite compat)
with op.batch_alter_table("devices") as batch_op:
batch_op.add_column(sa.Column("category_id", sa.Integer, nullable=True))
def downgrade() -> None:
op.drop_column("devices", "category_id")
op.drop_table("energy_categories")

View File

@@ -0,0 +1,168 @@
"""Add charging tables
Revision ID: 004_charging
Revises: 003_energy_categories
Create Date: 2026-04-03
"""
from alembic import op
import sqlalchemy as sa
revision = "004_charging"
down_revision = "003_energy_categories"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- charging_merchants ---
op.create_table(
"charging_merchants",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("contact_person", sa.String(100)),
sa.Column("phone", sa.String(20)),
sa.Column("email", sa.String(100)),
sa.Column("address", sa.String(500)),
sa.Column("business_license", sa.String(100)),
sa.Column("status", sa.String(20), default="active"),
sa.Column("settlement_type", sa.String(20)),
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()),
)
# --- charging_brands ---
op.create_table(
"charging_brands",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("brand_name", sa.String(100), nullable=False),
sa.Column("logo_url", sa.String(500)),
sa.Column("country", sa.String(50)),
sa.Column("description", sa.Text),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- charging_stations ---
op.create_table(
"charging_stations",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("merchant_id", sa.Integer, sa.ForeignKey("charging_merchants.id")),
sa.Column("type", sa.String(50)),
sa.Column("address", sa.String(500)),
sa.Column("latitude", sa.Float),
sa.Column("longitude", sa.Float),
sa.Column("price", sa.Float),
sa.Column("activity", sa.Text),
sa.Column("status", sa.String(20), default="active"),
sa.Column("total_piles", sa.Integer, default=0),
sa.Column("available_piles", sa.Integer, default=0),
sa.Column("total_power_kw", sa.Float, default=0),
sa.Column("photo_url", sa.String(500)),
sa.Column("operating_hours", sa.String(100)),
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()),
)
# --- charging_piles ---
op.create_table(
"charging_piles",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("station_id", sa.Integer, sa.ForeignKey("charging_stations.id"), nullable=False),
sa.Column("encoding", sa.String(100), unique=True),
sa.Column("name", sa.String(200)),
sa.Column("type", sa.String(50)),
sa.Column("brand", sa.String(100)),
sa.Column("model", sa.String(100)),
sa.Column("rated_power_kw", sa.Float),
sa.Column("connector_type", sa.String(50)),
sa.Column("status", sa.String(20), default="active"),
sa.Column("work_status", sa.String(20), default="offline"),
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()),
)
# --- charging_price_strategies ---
op.create_table(
"charging_price_strategies",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("strategy_name", sa.String(200), nullable=False),
sa.Column("station_id", sa.Integer, sa.ForeignKey("charging_stations.id")),
sa.Column("bill_model", sa.String(20)),
sa.Column("description", sa.Text),
sa.Column("status", sa.String(20), default="inactive"),
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()),
)
# --- charging_price_params ---
op.create_table(
"charging_price_params",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("strategy_id", sa.Integer, sa.ForeignKey("charging_price_strategies.id"), nullable=False),
sa.Column("start_time", sa.String(10), nullable=False),
sa.Column("end_time", sa.String(10), nullable=False),
sa.Column("period_mark", sa.String(20)),
sa.Column("elec_price", sa.Float, nullable=False),
sa.Column("service_price", sa.Float, default=0),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- charging_orders ---
op.create_table(
"charging_orders",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("order_no", sa.String(50), unique=True, nullable=False),
sa.Column("user_id", sa.Integer),
sa.Column("user_name", sa.String(100)),
sa.Column("phone", sa.String(20)),
sa.Column("station_id", sa.Integer, sa.ForeignKey("charging_stations.id")),
sa.Column("station_name", sa.String(200)),
sa.Column("pile_id", sa.Integer, sa.ForeignKey("charging_piles.id")),
sa.Column("pile_name", sa.String(200)),
sa.Column("start_time", sa.DateTime(timezone=True)),
sa.Column("end_time", sa.DateTime(timezone=True)),
sa.Column("car_no", sa.String(20)),
sa.Column("car_vin", sa.String(50)),
sa.Column("charge_method", sa.String(20)),
sa.Column("settle_type", sa.String(20)),
sa.Column("pay_type", sa.String(20)),
sa.Column("settle_time", sa.DateTime(timezone=True)),
sa.Column("settle_price", sa.Float),
sa.Column("paid_price", sa.Float),
sa.Column("discount_amt", sa.Float, default=0),
sa.Column("elec_amt", sa.Float),
sa.Column("serve_amt", sa.Float),
sa.Column("order_status", sa.String(20), default="charging"),
sa.Column("charge_duration", sa.Integer),
sa.Column("energy", sa.Float),
sa.Column("start_soc", sa.Float),
sa.Column("end_soc", sa.Float),
sa.Column("abno_cause", sa.Text),
sa.Column("order_source", sa.String(20)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- occupancy_orders ---
op.create_table(
"occupancy_orders",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("order_id", sa.Integer, sa.ForeignKey("charging_orders.id")),
sa.Column("pile_id", sa.Integer, sa.ForeignKey("charging_piles.id")),
sa.Column("start_time", sa.DateTime(timezone=True)),
sa.Column("end_time", sa.DateTime(timezone=True)),
sa.Column("occupancy_fee", sa.Float, default=0),
sa.Column("status", sa.String(20), default="active"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("occupancy_orders")
op.drop_table("charging_orders")
op.drop_table("charging_price_params")
op.drop_table("charging_price_strategies")
op.drop_table("charging_piles")
op.drop_table("charging_stations")
op.drop_table("charging_brands")
op.drop_table("charging_merchants")

View File

@@ -0,0 +1,53 @@
"""Add quota tables
Revision ID: 005_quota
Revises: 004_charging
Create Date: 2026-04-03
"""
from alembic import op
import sqlalchemy as sa
revision = "005_quota"
down_revision = "004_charging"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- energy_quotas ---
op.create_table(
"energy_quotas",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("target_type", sa.String(50), nullable=False),
sa.Column("target_id", sa.Integer, nullable=False),
sa.Column("energy_type", sa.String(50), nullable=False),
sa.Column("period", sa.String(20), nullable=False),
sa.Column("quota_value", sa.Float, nullable=False),
sa.Column("unit", sa.String(20), default="kWh"),
sa.Column("warning_threshold_pct", sa.Float, default=80),
sa.Column("alert_threshold_pct", sa.Float, default=95),
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()),
)
# --- quota_usage ---
op.create_table(
"quota_usage",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("quota_id", sa.Integer, sa.ForeignKey("energy_quotas.id"), nullable=False),
sa.Column("period_start", sa.DateTime(timezone=True), nullable=False),
sa.Column("period_end", sa.DateTime(timezone=True), nullable=False),
sa.Column("actual_value", sa.Float, default=0),
sa.Column("quota_value", sa.Float, nullable=False),
sa.Column("usage_rate_pct", sa.Float, default=0),
sa.Column("status", sa.String(20), default="normal"),
sa.Column("calculated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("quota_usage")
op.drop_table("energy_quotas")

View File

@@ -0,0 +1,48 @@
"""Add pricing tables
Revision ID: 006_pricing
Revises: 005_quota
Create Date: 2026-04-03
"""
from alembic import op
import sqlalchemy as sa
revision = "006_pricing"
down_revision = "005_quota"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- electricity_pricing ---
op.create_table(
"electricity_pricing",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("energy_type", sa.String(50), default="electricity"),
sa.Column("pricing_type", sa.String(20), nullable=False),
sa.Column("effective_from", sa.DateTime(timezone=True)),
sa.Column("effective_to", sa.DateTime(timezone=True)),
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()),
)
# --- pricing_periods ---
op.create_table(
"pricing_periods",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("pricing_id", sa.Integer, sa.ForeignKey("electricity_pricing.id"), nullable=False),
sa.Column("period_name", sa.String(50), nullable=False),
sa.Column("start_time", sa.String(10), nullable=False),
sa.Column("end_time", sa.String(10), nullable=False),
sa.Column("price_per_unit", sa.Float, nullable=False),
sa.Column("applicable_months", sa.JSON),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("pricing_periods")
op.drop_table("electricity_pricing")

View File

@@ -0,0 +1,88 @@
"""Add maintenance tables
Revision ID: 007_maintenance
Revises: 006_pricing
Create Date: 2026-04-03
"""
from alembic import op
import sqlalchemy as sa
revision = "007_maintenance"
down_revision = "006_pricing"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- inspection_plans ---
op.create_table(
"inspection_plans",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("description", sa.Text),
sa.Column("device_group_id", sa.Integer, sa.ForeignKey("device_groups.id")),
sa.Column("device_ids", sa.JSON),
sa.Column("schedule_type", sa.String(20)),
sa.Column("schedule_cron", sa.String(100)),
sa.Column("inspector_id", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("checklist", sa.JSON),
sa.Column("is_active", sa.Boolean, default=True),
sa.Column("next_run_at", sa.DateTime(timezone=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()),
)
# --- inspection_records ---
op.create_table(
"inspection_records",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("plan_id", sa.Integer, sa.ForeignKey("inspection_plans.id"), nullable=False),
sa.Column("inspector_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("status", sa.String(20), default="pending"),
sa.Column("findings", sa.JSON),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# --- repair_orders ---
op.create_table(
"repair_orders",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("code", sa.String(50), unique=True, nullable=False),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("description", sa.Text),
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id")),
sa.Column("alarm_event_id", sa.Integer, sa.ForeignKey("alarm_events.id")),
sa.Column("priority", sa.String(20), default="medium"),
sa.Column("status", sa.String(20), default="open"),
sa.Column("assigned_to", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("resolution", sa.Text),
sa.Column("cost_estimate", sa.Float),
sa.Column("actual_cost", sa.Float),
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("assigned_at", sa.DateTime(timezone=True)),
sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.Column("closed_at", sa.DateTime(timezone=True)),
)
# --- duty_schedules ---
op.create_table(
"duty_schedules",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("duty_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("shift", sa.String(20)),
sa.Column("area_id", sa.Integer, sa.ForeignKey("device_groups.id")),
sa.Column("notes", sa.Text),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("duty_schedules")
op.drop_table("repair_orders")
op.drop_table("inspection_records")
op.drop_table("inspection_plans")

View File

@@ -0,0 +1,79 @@
"""Add management tables
Revision ID: 008_management
Revises: 007_maintenance
Create Date: 2026-04-03
"""
from alembic import op
import sqlalchemy as sa
revision = "008_management"
down_revision = "007_maintenance"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- regulations ---
op.create_table(
"regulations",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("category", sa.String(50)),
sa.Column("content", sa.Text),
sa.Column("effective_date", sa.DateTime(timezone=True)),
sa.Column("status", sa.String(20), default="active"),
sa.Column("attachment_url", sa.String(500)),
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()),
)
# --- standards ---
op.create_table(
"standards",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("code", sa.String(100)),
sa.Column("type", sa.String(50)),
sa.Column("description", sa.Text),
sa.Column("compliance_status", sa.String(20), default="pending"),
sa.Column("review_date", sa.DateTime(timezone=True)),
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()),
)
# --- process_docs ---
op.create_table(
"process_docs",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("category", sa.String(50)),
sa.Column("content", sa.Text),
sa.Column("version", sa.String(20), default="1.0"),
sa.Column("approved_by", sa.String(100)),
sa.Column("effective_date", sa.DateTime(timezone=True)),
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()),
)
# --- emergency_plans ---
op.create_table(
"emergency_plans",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("scenario", sa.String(100)),
sa.Column("steps", sa.JSON),
sa.Column("responsible_person", sa.String(100)),
sa.Column("review_date", sa.DateTime(timezone=True)),
sa.Column("is_active", sa.Boolean, default=True),
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()),
)
def downgrade() -> None:
op.drop_table("emergency_plans")
op.drop_table("process_docs")
op.drop_table("standards")
op.drop_table("regulations")