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

490 lines
17 KiB
Python
Raw Normal View History

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,
}