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

View File

@@ -0,0 +1,385 @@
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from pydantic import BaseModel
from app.core.database import get_db
from app.core.deps import get_current_user, require_roles
from app.models.management import Regulation, Standard, ProcessDoc, EmergencyPlan
from app.models.user import User
router = APIRouter(prefix="/management", tags=["管理体系"])
# ── Pydantic Schemas ──────────────────────────────────────────────────
class RegulationCreate(BaseModel):
title: str
category: str | None = None
content: str | None = None
effective_date: datetime | None = None
status: str = "active"
attachment_url: str | None = None
class StandardCreate(BaseModel):
name: str
code: str | None = None
type: str | None = None
description: str | None = None
compliance_status: str = "pending"
review_date: datetime | None = None
class ProcessDocCreate(BaseModel):
title: str
category: str | None = None
content: str | None = None
version: str = "1.0"
approved_by: str | None = None
effective_date: datetime | None = None
class EmergencyPlanCreate(BaseModel):
title: str
scenario: str | None = None
steps: list[dict] | None = None
responsible_person: str | None = None
review_date: datetime | None = None
is_active: bool = True
# ── Regulations (规章制度) ────────────────────────────────────────────
@router.get("/regulations")
async def list_regulations(
category: str | None = None,
status: str | None = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
query = select(Regulation)
if category:
query = query.where(Regulation.category == category)
if status:
query = query.where(Regulation.status == status)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(Regulation.id.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
return {
"total": total,
"items": [_regulation_to_dict(r) for r in result.scalars().all()],
}
@router.post("/regulations")
async def create_regulation(
data: RegulationCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
reg = Regulation(**data.model_dump(), created_by=user.id)
db.add(reg)
await db.flush()
return _regulation_to_dict(reg)
@router.put("/regulations/{reg_id}")
async def update_regulation(
reg_id: int,
data: RegulationCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(Regulation).where(Regulation.id == reg_id))
reg = result.scalar_one_or_none()
if not reg:
raise HTTPException(status_code=404, detail="规章制度不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(reg, k, v)
return _regulation_to_dict(reg)
@router.delete("/regulations/{reg_id}")
async def delete_regulation(
reg_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(Regulation).where(Regulation.id == reg_id))
reg = result.scalar_one_or_none()
if not reg:
raise HTTPException(status_code=404, detail="规章制度不存在")
await db.delete(reg)
return {"message": "已删除"}
# ── Standards (标准规范) ──────────────────────────────────────────────
@router.get("/standards")
async def list_standards(
type: str | None = None,
compliance_status: str | None = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
query = select(Standard)
if type:
query = query.where(Standard.type == type)
if compliance_status:
query = query.where(Standard.compliance_status == compliance_status)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(Standard.id.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
return {
"total": total,
"items": [_standard_to_dict(s) for s in result.scalars().all()],
}
@router.post("/standards")
async def create_standard(
data: StandardCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
std = Standard(**data.model_dump())
db.add(std)
await db.flush()
return _standard_to_dict(std)
@router.put("/standards/{std_id}")
async def update_standard(
std_id: int,
data: StandardCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(Standard).where(Standard.id == std_id))
std = result.scalar_one_or_none()
if not std:
raise HTTPException(status_code=404, detail="标准规范不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(std, k, v)
return _standard_to_dict(std)
@router.delete("/standards/{std_id}")
async def delete_standard(
std_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(Standard).where(Standard.id == std_id))
std = result.scalar_one_or_none()
if not std:
raise HTTPException(status_code=404, detail="标准规范不存在")
await db.delete(std)
return {"message": "已删除"}
# ── Process Docs (管理流程) ───────────────────────────────────────────
@router.get("/process-docs")
async def list_process_docs(
category: str | None = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
query = select(ProcessDoc)
if category:
query = query.where(ProcessDoc.category == category)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(ProcessDoc.id.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
return {
"total": total,
"items": [_process_doc_to_dict(d) for d in result.scalars().all()],
}
@router.post("/process-docs")
async def create_process_doc(
data: ProcessDocCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
doc = ProcessDoc(**data.model_dump())
db.add(doc)
await db.flush()
return _process_doc_to_dict(doc)
@router.put("/process-docs/{doc_id}")
async def update_process_doc(
doc_id: int,
data: ProcessDocCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(ProcessDoc).where(ProcessDoc.id == doc_id))
doc = result.scalar_one_or_none()
if not doc:
raise HTTPException(status_code=404, detail="管理流程文档不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(doc, k, v)
return _process_doc_to_dict(doc)
@router.delete("/process-docs/{doc_id}")
async def delete_process_doc(
doc_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(ProcessDoc).where(ProcessDoc.id == doc_id))
doc = result.scalar_one_or_none()
if not doc:
raise HTTPException(status_code=404, detail="管理流程文档不存在")
await db.delete(doc)
return {"message": "已删除"}
# ── Emergency Plans (应急预案) ────────────────────────────────────────
@router.get("/emergency-plans")
async def list_emergency_plans(
scenario: str | None = None,
is_active: bool | None = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
query = select(EmergencyPlan)
if scenario:
query = query.where(EmergencyPlan.scenario == scenario)
if is_active is not None:
query = query.where(EmergencyPlan.is_active == is_active)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(EmergencyPlan.id.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
return {
"total": total,
"items": [_emergency_plan_to_dict(p) for p in result.scalars().all()],
}
@router.post("/emergency-plans")
async def create_emergency_plan(
data: EmergencyPlanCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
plan = EmergencyPlan(**data.model_dump())
db.add(plan)
await db.flush()
return _emergency_plan_to_dict(plan)
@router.put("/emergency-plans/{plan_id}")
async def update_emergency_plan(
plan_id: int,
data: EmergencyPlanCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(EmergencyPlan).where(EmergencyPlan.id == plan_id))
plan = result.scalar_one_or_none()
if not plan:
raise HTTPException(status_code=404, detail="应急预案不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(plan, k, v)
return _emergency_plan_to_dict(plan)
@router.delete("/emergency-plans/{plan_id}")
async def delete_emergency_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(EmergencyPlan).where(EmergencyPlan.id == plan_id))
plan = result.scalar_one_or_none()
if not plan:
raise HTTPException(status_code=404, detail="应急预案不存在")
await db.delete(plan)
return {"message": "已删除"}
# ── Compliance Overview ───────────────────────────────────────────────
@router.get("/compliance")
async def compliance_overview(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""合规概览 - count by compliance_status for standards"""
result = await db.execute(
select(Standard.compliance_status, func.count(Standard.id))
.group_by(Standard.compliance_status)
)
stats = {row[0]: row[1] for row in result.all()}
return {
"compliant": stats.get("compliant", 0),
"non_compliant": stats.get("non_compliant", 0),
"pending": stats.get("pending", 0),
"in_progress": stats.get("in_progress", 0),
"total": sum(stats.values()),
}
# ── Serializers ───────────────────────────────────────────────────────
def _regulation_to_dict(r: Regulation) -> dict:
return {
"id": r.id, "title": r.title, "category": r.category,
"content": r.content, "effective_date": str(r.effective_date) if r.effective_date else None,
"status": r.status, "attachment_url": r.attachment_url,
"created_by": r.created_by,
"created_at": str(r.created_at) if r.created_at else None,
"updated_at": str(r.updated_at) if r.updated_at else None,
}
def _standard_to_dict(s: Standard) -> dict:
return {
"id": s.id, "name": s.name, "code": s.code, "type": s.type,
"description": s.description, "compliance_status": s.compliance_status,
"review_date": str(s.review_date) if s.review_date else None,
"created_at": str(s.created_at) if s.created_at else None,
"updated_at": str(s.updated_at) if s.updated_at else None,
}
def _process_doc_to_dict(d: ProcessDoc) -> dict:
return {
"id": d.id, "title": d.title, "category": d.category,
"content": d.content, "version": d.version,
"approved_by": d.approved_by,
"effective_date": str(d.effective_date) if d.effective_date else None,
"created_at": str(d.created_at) if d.created_at else None,
"updated_at": str(d.updated_at) if d.updated_at else None,
}
def _emergency_plan_to_dict(p: EmergencyPlan) -> dict:
return {
"id": p.id, "title": p.title, "scenario": p.scenario,
"steps": p.steps, "responsible_person": p.responsible_person,
"review_date": str(p.review_date) if p.review_date else None,
"is_active": p.is_active,
"created_at": str(p.created_at) if p.created_at else None,
"updated_at": str(p.updated_at) if p.updated_at else None,
}