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