Files
tp-ems/core/backend/app/api/v1/maintenance.py

490 lines
17 KiB
Python

from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from pydantic import BaseModel
from app.core.database import get_db
from app.core.deps import get_current_user, require_roles
from app.models.maintenance import InspectionPlan, InspectionRecord, RepairOrder, DutySchedule
from app.models.user import User
router = APIRouter(prefix="/maintenance", tags=["运维管理"])
# ── Pydantic Schemas ────────────────────────────────────────────────
class PlanCreate(BaseModel):
name: str
description: str | None = None
device_group_id: int | None = None
device_ids: list[int] | None = None
schedule_type: str | None = None
schedule_cron: str | None = None
inspector_id: int | None = None
checklist: list[dict] | None = None
is_active: bool = True
next_run_at: str | None = None
class RecordCreate(BaseModel):
plan_id: int
inspector_id: int
status: str = "pending"
findings: list[dict] | None = None
started_at: str | None = None
class RecordUpdate(BaseModel):
status: str | None = None
findings: list[dict] | None = None
completed_at: str | None = None
class OrderCreate(BaseModel):
title: str
description: str | None = None
device_id: int | None = None
alarm_event_id: int | None = None
priority: str = "medium"
cost_estimate: float | None = None
class OrderUpdate(BaseModel):
title: str | None = None
description: str | None = None
priority: str | None = None
status: str | None = None
resolution: str | None = None
actual_cost: float | None = None
class DutyCreate(BaseModel):
user_id: int
duty_date: str
shift: str | None = None
area_id: int | None = None
notes: str | None = None
# ── Helpers ─────────────────────────────────────────────────────────
def _plan_to_dict(p: InspectionPlan) -> dict:
return {
"id": p.id, "name": p.name, "description": p.description,
"device_group_id": p.device_group_id, "device_ids": p.device_ids,
"schedule_type": p.schedule_type, "schedule_cron": p.schedule_cron,
"inspector_id": p.inspector_id, "checklist": p.checklist,
"is_active": p.is_active,
"next_run_at": str(p.next_run_at) if p.next_run_at else None,
"created_by": p.created_by,
"created_at": str(p.created_at) if p.created_at else None,
"updated_at": str(p.updated_at) if p.updated_at else None,
}
def _record_to_dict(r: InspectionRecord) -> dict:
return {
"id": r.id, "plan_id": r.plan_id, "inspector_id": r.inspector_id,
"status": r.status, "findings": r.findings,
"started_at": str(r.started_at) if r.started_at else None,
"completed_at": str(r.completed_at) if r.completed_at else None,
"created_at": str(r.created_at) if r.created_at else None,
}
def _order_to_dict(o: RepairOrder) -> dict:
return {
"id": o.id, "code": o.code, "title": o.title, "description": o.description,
"device_id": o.device_id, "alarm_event_id": o.alarm_event_id,
"priority": o.priority, "status": o.status, "assigned_to": o.assigned_to,
"resolution": o.resolution, "cost_estimate": o.cost_estimate,
"actual_cost": o.actual_cost, "created_by": o.created_by,
"created_at": str(o.created_at) if o.created_at else None,
"assigned_at": str(o.assigned_at) if o.assigned_at else None,
"completed_at": str(o.completed_at) if o.completed_at else None,
"closed_at": str(o.closed_at) if o.closed_at else None,
}
def _duty_to_dict(d: DutySchedule) -> dict:
return {
"id": d.id, "user_id": d.user_id,
"duty_date": str(d.duty_date) if d.duty_date else None,
"shift": d.shift, "area_id": d.area_id, "notes": d.notes,
"created_at": str(d.created_at) if d.created_at else None,
}
def _generate_order_code() -> str:
now = datetime.now(timezone.utc)
return f"WO-{now.strftime('%Y%m%d')}-{now.strftime('%H%M%S')}"
# ── Inspection Plans ────────────────────────────────────────────────
@router.get("/plans")
async def list_plans(
is_active: bool | None = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
query = select(InspectionPlan).order_by(InspectionPlan.id.desc())
if is_active is not None:
query = query.where(InspectionPlan.is_active == is_active)
result = await db.execute(query)
return [_plan_to_dict(p) for p in result.scalars().all()]
@router.post("/plans")
async def create_plan(
data: PlanCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
plan = InspectionPlan(**data.model_dump(exclude={"next_run_at"}), created_by=user.id)
if data.next_run_at:
plan.next_run_at = datetime.fromisoformat(data.next_run_at)
db.add(plan)
await db.flush()
return _plan_to_dict(plan)
@router.put("/plans/{plan_id}")
async def update_plan(
plan_id: int,
data: PlanCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(InspectionPlan).where(InspectionPlan.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, exclude={"next_run_at"}).items():
setattr(plan, k, v)
if data.next_run_at:
plan.next_run_at = datetime.fromisoformat(data.next_run_at)
return _plan_to_dict(plan)
@router.delete("/plans/{plan_id}")
async def delete_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(InspectionPlan).where(InspectionPlan.id == plan_id))
plan = result.scalar_one_or_none()
if not plan:
raise HTTPException(status_code=404, detail="巡检计划不存在")
plan.is_active = False
return {"message": "已删除"}
@router.post("/plans/{plan_id}/trigger")
async def trigger_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
"""手动触发巡检计划,生成巡检记录"""
result = await db.execute(select(InspectionPlan).where(InspectionPlan.id == plan_id))
plan = result.scalar_one_or_none()
if not plan:
raise HTTPException(status_code=404, detail="巡检计划不存在")
record = InspectionRecord(
plan_id=plan.id,
inspector_id=plan.inspector_id or user.id,
status="pending",
)
db.add(record)
await db.flush()
return _record_to_dict(record)
# ── Inspection Records ──────────────────────────────────────────────
@router.get("/records")
async def list_records(
plan_id: int | 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(InspectionRecord)
if plan_id:
query = query.where(InspectionRecord.plan_id == plan_id)
if status:
query = query.where(InspectionRecord.status == status)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(InspectionRecord.id.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
return {
"total": total,
"items": [_record_to_dict(r) for r in result.scalars().all()],
}
@router.post("/records")
async def create_record(
data: RecordCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
record = InspectionRecord(**data.model_dump(exclude={"started_at"}))
if data.started_at:
record.started_at = datetime.fromisoformat(data.started_at)
db.add(record)
await db.flush()
return _record_to_dict(record)
@router.put("/records/{record_id}")
async def update_record(
record_id: int,
data: RecordUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
result = await db.execute(select(InspectionRecord).where(InspectionRecord.id == record_id))
record = result.scalar_one_or_none()
if not record:
raise HTTPException(status_code=404, detail="巡检记录不存在")
for k, v in data.model_dump(exclude_unset=True, exclude={"completed_at"}).items():
setattr(record, k, v)
if data.completed_at:
record.completed_at = datetime.fromisoformat(data.completed_at)
elif data.status == "completed" or data.status == "issues_found":
record.completed_at = datetime.now(timezone.utc)
return _record_to_dict(record)
# ── Repair Orders ───────────────────────────────────────────────────
@router.get("/orders")
async def list_orders(
status: str | None = None,
priority: 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(RepairOrder)
if status:
query = query.where(RepairOrder.status == status)
if priority:
query = query.where(RepairOrder.priority == priority)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(RepairOrder.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
return {
"total": total,
"items": [_order_to_dict(o) for o in result.scalars().all()],
}
@router.post("/orders")
async def create_order(
data: OrderCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
order = RepairOrder(
**data.model_dump(),
code=_generate_order_code(),
created_by=user.id,
)
db.add(order)
await db.flush()
return _order_to_dict(order)
@router.put("/orders/{order_id}")
async def update_order(
order_id: int,
data: OrderUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
result = await db.execute(select(RepairOrder).where(RepairOrder.id == order_id))
order = result.scalar_one_or_none()
if not order:
raise HTTPException(status_code=404, detail="工单不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(order, k, v)
return _order_to_dict(order)
@router.put("/orders/{order_id}/assign")
async def assign_order(
order_id: int,
assigned_to: int = Query(..., description="指派的用户ID"),
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(RepairOrder).where(RepairOrder.id == order_id))
order = result.scalar_one_or_none()
if not order:
raise HTTPException(status_code=404, detail="工单不存在")
order.assigned_to = assigned_to
order.status = "assigned"
order.assigned_at = datetime.now(timezone.utc)
return _order_to_dict(order)
@router.put("/orders/{order_id}/complete")
async def complete_order(
order_id: int,
resolution: str = "",
actual_cost: float | None = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
result = await db.execute(select(RepairOrder).where(RepairOrder.id == order_id))
order = result.scalar_one_or_none()
if not order:
raise HTTPException(status_code=404, detail="工单不存在")
order.status = "completed"
order.resolution = resolution
if actual_cost is not None:
order.actual_cost = actual_cost
order.completed_at = datetime.now(timezone.utc)
return _order_to_dict(order)
# ── Duty Schedule ───────────────────────────────────────────────────
@router.get("/duty")
async def list_duty(
start_date: str | None = None,
end_date: str | None = None,
user_id: int | None = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
query = select(DutySchedule)
if start_date:
query = query.where(DutySchedule.duty_date >= start_date)
if end_date:
query = query.where(DutySchedule.duty_date <= end_date)
if user_id:
query = query.where(DutySchedule.user_id == user_id)
query = query.order_by(DutySchedule.duty_date)
result = await db.execute(query)
return [_duty_to_dict(d) for d in result.scalars().all()]
@router.post("/duty")
async def create_duty(
data: DutyCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
duty = DutySchedule(
user_id=data.user_id,
duty_date=datetime.fromisoformat(data.duty_date),
shift=data.shift,
area_id=data.area_id,
notes=data.notes,
)
db.add(duty)
await db.flush()
return _duty_to_dict(duty)
@router.put("/duty/{duty_id}")
async def update_duty(
duty_id: int,
data: DutyCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(DutySchedule).where(DutySchedule.id == duty_id))
duty = result.scalar_one_or_none()
if not duty:
raise HTTPException(status_code=404, detail="值班记录不存在")
duty.user_id = data.user_id
duty.duty_date = datetime.fromisoformat(data.duty_date)
duty.shift = data.shift
duty.area_id = data.area_id
duty.notes = data.notes
return _duty_to_dict(duty)
@router.delete("/duty/{duty_id}")
async def delete_duty(
duty_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_roles("admin", "energy_manager")),
):
result = await db.execute(select(DutySchedule).where(DutySchedule.id == duty_id))
duty = result.scalar_one_or_none()
if not duty:
raise HTTPException(status_code=404, detail="值班记录不存在")
await db.delete(duty)
return {"message": "已删除"}
# ── Dashboard ───────────────────────────────────────────────────────
@router.get("/dashboard")
async def maintenance_dashboard(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
now = datetime.now(timezone.utc)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
# Open orders count
open_q = select(func.count()).select_from(RepairOrder).where(
RepairOrder.status.in_(["open", "assigned", "in_progress"])
)
open_orders = (await db.execute(open_q)).scalar() or 0
# Overdue: assigned but not completed, assigned_at > 7 days ago
from datetime import timedelta
overdue_cutoff = now - timedelta(days=7)
overdue_q = select(func.count()).select_from(RepairOrder).where(
and_(
RepairOrder.status.in_(["assigned", "in_progress"]),
RepairOrder.assigned_at < overdue_cutoff,
)
)
overdue_count = (await db.execute(overdue_q)).scalar() or 0
# Today's inspections
inspect_q = select(func.count()).select_from(InspectionRecord).where(
InspectionRecord.created_at >= today_start,
)
todays_inspections = (await db.execute(inspect_q)).scalar() or 0
# Upcoming duties (next 7 days)
duty_end = now + timedelta(days=7)
duty_q = select(func.count()).select_from(DutySchedule).where(
and_(DutySchedule.duty_date >= today_start, DutySchedule.duty_date <= duty_end)
)
upcoming_duties = (await db.execute(duty_q)).scalar() or 0
# Recent orders (latest 10)
recent_q = select(RepairOrder).order_by(RepairOrder.created_at.desc()).limit(10)
recent_result = await db.execute(recent_q)
recent_orders = [_order_to_dict(o) for o in recent_result.scalars().all()]
return {
"open_orders": open_orders,
"overdue_count": overdue_count,
"todays_inspections": todays_inspections,
"upcoming_duties": upcoming_duties,
"recent_orders": recent_orders,
}