feat: v2.0 — maintenance module, AI analysis, station power fix
- Add full 检修维护中心 (6.4): 3-type work orders (消缺/巡检/抄表), asset management, warehouse, work plans, billing settlement - Add AI智能分析 tab with LLM-powered diagnostics (StepFun + ZhipuAI) - Add AI模型配置 settings page (provider, temperature, prompts) - Fix station power accuracy: use API station total (station_power) instead of inverter-level computation — eliminates timing gaps - Add 7 new DB models, 4 new API routers, 5 new frontend pages - Migrations: 009 (maintenance expansion) + 010 (AI analysis) - Version bump: 1.6.1 → 2.0.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
167
core/backend/alembic/versions/009_maintenance_expansion.py
Normal file
167
core/backend/alembic/versions/009_maintenance_expansion.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""Add maintenance expansion tables and order_type
|
||||
|
||||
Revision ID: 009_maintenance_expansion
|
||||
Revises: 008_management
|
||||
Create Date: 2026-04-11
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "009_maintenance_expansion"
|
||||
down_revision = "008_management"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- Add order_type, station_name, due_date to repair_orders ---
|
||||
op.add_column("repair_orders", sa.Column("order_type", sa.String(20), server_default="repair"))
|
||||
op.add_column("repair_orders", sa.Column("station_name", sa.String(200)))
|
||||
op.add_column("repair_orders", sa.Column("due_date", sa.DateTime(timezone=True)))
|
||||
|
||||
# --- asset_categories ---
|
||||
op.create_table(
|
||||
"asset_categories",
|
||||
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("asset_categories.id"), nullable=True),
|
||||
sa.Column("description", sa.Text),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# --- assets ---
|
||||
op.create_table(
|
||||
"assets",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("name", sa.String(200), nullable=False),
|
||||
sa.Column("asset_code", sa.String(50), unique=True),
|
||||
sa.Column("category_id", sa.Integer, sa.ForeignKey("asset_categories.id")),
|
||||
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=True),
|
||||
sa.Column("station_name", sa.String(200)),
|
||||
sa.Column("manufacturer", sa.String(200)),
|
||||
sa.Column("model_number", sa.String(100)),
|
||||
sa.Column("serial_number", sa.String(100)),
|
||||
sa.Column("purchase_date", sa.DateTime(timezone=True)),
|
||||
sa.Column("warranty_expiry", sa.DateTime(timezone=True)),
|
||||
sa.Column("purchase_price", sa.Float),
|
||||
sa.Column("status", sa.String(20), server_default="active"),
|
||||
sa.Column("location", sa.String(200)),
|
||||
sa.Column("responsible_dept", sa.String(100)),
|
||||
sa.Column("custodian", sa.String(100)),
|
||||
sa.Column("supplier", sa.String(200)),
|
||||
sa.Column("notes", sa.Text),
|
||||
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()),
|
||||
)
|
||||
|
||||
# --- asset_changes ---
|
||||
op.create_table(
|
||||
"asset_changes",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("asset_id", sa.Integer, sa.ForeignKey("assets.id"), nullable=False),
|
||||
sa.Column("change_type", sa.String(20)),
|
||||
sa.Column("change_date", sa.DateTime(timezone=True)),
|
||||
sa.Column("description", sa.Text),
|
||||
sa.Column("old_value", sa.JSON),
|
||||
sa.Column("new_value", sa.JSON),
|
||||
sa.Column("operator_id", sa.Integer, sa.ForeignKey("users.id")),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# --- spare_parts ---
|
||||
op.create_table(
|
||||
"spare_parts",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("name", sa.String(200), nullable=False),
|
||||
sa.Column("part_code", sa.String(50), unique=True),
|
||||
sa.Column("category", sa.String(100)),
|
||||
sa.Column("specification", sa.String(200)),
|
||||
sa.Column("unit", sa.String(20)),
|
||||
sa.Column("current_stock", sa.Integer, server_default="0"),
|
||||
sa.Column("min_stock", sa.Integer, server_default="0"),
|
||||
sa.Column("max_stock", sa.Integer),
|
||||
sa.Column("warehouse_location", sa.String(100)),
|
||||
sa.Column("unit_price", sa.Float),
|
||||
sa.Column("supplier", sa.String(200)),
|
||||
sa.Column("notes", sa.Text),
|
||||
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()),
|
||||
)
|
||||
|
||||
# --- warehouse_transactions ---
|
||||
op.create_table(
|
||||
"warehouse_transactions",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("spare_part_id", sa.Integer, sa.ForeignKey("spare_parts.id"), nullable=False),
|
||||
sa.Column("transaction_type", sa.String(20)),
|
||||
sa.Column("quantity", sa.Integer, nullable=False),
|
||||
sa.Column("unit_price", sa.Float),
|
||||
sa.Column("total_price", sa.Float),
|
||||
sa.Column("transaction_date", sa.DateTime(timezone=True)),
|
||||
sa.Column("work_order_id", sa.Integer, sa.ForeignKey("repair_orders.id"), nullable=True),
|
||||
sa.Column("reason", sa.String(200)),
|
||||
sa.Column("operator_id", sa.Integer, sa.ForeignKey("users.id")),
|
||||
sa.Column("notes", sa.Text),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# --- maintenance_work_plans ---
|
||||
op.create_table(
|
||||
"maintenance_work_plans",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("name", sa.String(200), nullable=False),
|
||||
sa.Column("plan_type", sa.String(20)),
|
||||
sa.Column("station_name", sa.String(200)),
|
||||
sa.Column("device_ids", sa.JSON),
|
||||
sa.Column("cycle_period", sa.String(20)),
|
||||
sa.Column("execution_days", sa.Integer),
|
||||
sa.Column("effective_start", sa.DateTime(timezone=True)),
|
||||
sa.Column("effective_end", sa.DateTime(timezone=True)),
|
||||
sa.Column("description", sa.Text),
|
||||
sa.Column("workflow_config", sa.JSON),
|
||||
sa.Column("is_active", sa.Boolean, server_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()),
|
||||
)
|
||||
|
||||
# --- billing_records ---
|
||||
op.create_table(
|
||||
"billing_records",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("station_name", sa.String(200)),
|
||||
sa.Column("billing_type", sa.String(20)),
|
||||
sa.Column("billing_period_start", sa.DateTime(timezone=True)),
|
||||
sa.Column("billing_period_end", sa.DateTime(timezone=True)),
|
||||
sa.Column("generation_kwh", sa.Float),
|
||||
sa.Column("self_use_kwh", sa.Float),
|
||||
sa.Column("grid_feed_kwh", sa.Float),
|
||||
sa.Column("electricity_price", sa.Float),
|
||||
sa.Column("self_use_price", sa.Float),
|
||||
sa.Column("feed_in_tariff", sa.Float),
|
||||
sa.Column("total_amount", sa.Float),
|
||||
sa.Column("self_use_amount", sa.Float),
|
||||
sa.Column("feed_in_amount", sa.Float),
|
||||
sa.Column("subsidy_amount", sa.Float),
|
||||
sa.Column("status", sa.String(20), server_default="pending"),
|
||||
sa.Column("invoice_number", sa.String(100)),
|
||||
sa.Column("invoice_date", sa.DateTime(timezone=True)),
|
||||
sa.Column("attachment_url", sa.String(500)),
|
||||
sa.Column("notes", sa.Text),
|
||||
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()),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("billing_records")
|
||||
op.drop_table("maintenance_work_plans")
|
||||
op.drop_table("warehouse_transactions")
|
||||
op.drop_table("spare_parts")
|
||||
op.drop_table("asset_changes")
|
||||
op.drop_table("assets")
|
||||
op.drop_table("asset_categories")
|
||||
op.drop_column("repair_orders", "due_date")
|
||||
op.drop_column("repair_orders", "station_name")
|
||||
op.drop_column("repair_orders", "order_type")
|
||||
35
core/backend/alembic/versions/010_ai_analysis.py
Normal file
35
core/backend/alembic/versions/010_ai_analysis.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Add AI analysis results table
|
||||
|
||||
Revision ID: 010_ai_analysis
|
||||
Revises: 009_maintenance_expansion
|
||||
Create Date: 2026-04-11
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "010_ai_analysis"
|
||||
down_revision = "009_maintenance_expansion"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"ai_analysis_results",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("scope", sa.String(20), server_default="station"),
|
||||
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=True),
|
||||
sa.Column("analysis_type", sa.String(20), server_default="diagnostic"),
|
||||
sa.Column("prompt_used", sa.Text),
|
||||
sa.Column("result_text", sa.Text),
|
||||
sa.Column("model_used", sa.String(100)),
|
||||
sa.Column("provider_used", sa.String(20)),
|
||||
sa.Column("tokens_used", sa.Integer),
|
||||
sa.Column("duration_ms", sa.Integer),
|
||||
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("ai_analysis_results")
|
||||
Reference in New Issue
Block a user