490 lines
17 KiB
Python
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,
|
|
}
|