Files
desungongpai/app/routers/fences.py

257 lines
9.4 KiB
Python
Raw Normal View History

"""Fences Router - geofence management API endpoints."""
import math
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import require_write
from app.database import get_db
from app.models import AttendanceRecord, DeviceFenceBinding, DeviceFenceState, FenceConfig
from app.schemas import (
APIResponse,
DeviceFenceBindRequest,
FenceConfigCreate,
FenceConfigResponse,
FenceConfigUpdate,
FenceDeviceDetail,
PaginatedList,
)
from app.services import fence_service
router = APIRouter(prefix="/api/fences", tags=["Fences"])
@router.get("", response_model=APIResponse[PaginatedList[FenceConfigResponse]])
async def list_fences(
is_active: bool | None = Query(default=None),
search: str | None = Query(default=None),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
):
records, total = await fence_service.get_fences(db, page, page_size, is_active, search)
return APIResponse(
data=PaginatedList(
items=[FenceConfigResponse.model_validate(r) for r in records],
total=total, page=page, page_size=page_size,
total_pages=math.ceil(total / page_size) if total else 0,
)
)
@router.get(
"/stats",
response_model=APIResponse[dict],
summary="围栏统计 / Fence statistics",
)
async def fence_stats(db: AsyncSession = Depends(get_db)):
"""
围栏统计总数活跃数绑定设备总数各围栏当前在内设备数今日进出事件数
Fence stats: totals, bindings, devices currently inside, today's events.
"""
from app.config import now_cst
now = now_cst()
total = (await db.execute(select(func.count(FenceConfig.id)))).scalar() or 0
active = (await db.execute(
select(func.count(FenceConfig.id)).where(FenceConfig.is_active == True) # noqa: E712
)).scalar() or 0
total_bindings = (await db.execute(select(func.count(DeviceFenceBinding.id)))).scalar() or 0
# Devices currently inside fences
inside_result = await db.execute(
select(
DeviceFenceState.fence_id,
func.count(DeviceFenceState.id),
)
.where(DeviceFenceState.is_inside == True) # noqa: E712
.group_by(DeviceFenceState.fence_id)
)
devices_inside = {row[0]: row[1] for row in inside_result.all()}
total_inside = sum(devices_inside.values())
# Today's fence attendance events
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
today_events = (await db.execute(
select(func.count(AttendanceRecord.id))
.where(
AttendanceRecord.attendance_source == "fence",
AttendanceRecord.recorded_at >= today_start,
)
)).scalar() or 0
# Per-fence summary
fence_result = await db.execute(
select(FenceConfig.id, FenceConfig.name, FenceConfig.fence_type, FenceConfig.is_active)
)
fence_summary = []
for row in fence_result.all():
fid = row[0]
fence_summary.append({
"fence_id": fid,
"name": row[1],
"type": row[2],
"is_active": bool(row[3]),
"devices_inside": devices_inside.get(fid, 0),
})
return APIResponse(data={
"total": total,
"active": active,
"inactive": total - active,
"total_bindings": total_bindings,
"total_devices_inside": total_inside,
"today_events": today_events,
"fences": fence_summary,
})
@router.get("/all-active", response_model=APIResponse[list[FenceConfigResponse]])
async def get_all_active(db: AsyncSession = Depends(get_db)):
fences = await fence_service.get_all_active_fences(db)
return APIResponse(data=[FenceConfigResponse.model_validate(f) for f in fences])
@router.get("/{fence_id}", response_model=APIResponse[FenceConfigResponse])
async def get_fence(fence_id: int, db: AsyncSession = Depends(get_db)):
fence = await fence_service.get_fence(db, fence_id)
if fence is None:
raise HTTPException(status_code=404, detail="Fence not found")
return APIResponse(data=FenceConfigResponse.model_validate(fence))
@router.post("", response_model=APIResponse[FenceConfigResponse], status_code=201, dependencies=[Depends(require_write)])
async def create_fence(body: FenceConfigCreate, db: AsyncSession = Depends(get_db)):
fence = await fence_service.create_fence(db, body)
return APIResponse(message="Fence created", data=FenceConfigResponse.model_validate(fence))
@router.put("/{fence_id}", response_model=APIResponse[FenceConfigResponse], dependencies=[Depends(require_write)])
async def update_fence(fence_id: int, body: FenceConfigUpdate, db: AsyncSession = Depends(get_db)):
fence = await fence_service.update_fence(db, fence_id, body)
if fence is None:
raise HTTPException(status_code=404, detail="Fence not found")
return APIResponse(message="Fence updated", data=FenceConfigResponse.model_validate(fence))
@router.delete("/{fence_id}", response_model=APIResponse, dependencies=[Depends(require_write)])
async def delete_fence(fence_id: int, db: AsyncSession = Depends(get_db)):
if not await fence_service.delete_fence(db, fence_id):
raise HTTPException(status_code=404, detail="Fence not found")
return APIResponse(message="Fence deleted")
@router.get(
"/{fence_id}/events",
response_model=APIResponse[PaginatedList[dict]],
summary="围栏进出事件历史 / Fence events history",
)
async def fence_events(
fence_id: int,
start_time: datetime | None = Query(default=None, description="开始时间"),
end_time: datetime | None = Query(default=None, description="结束时间"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
):
"""
获取围栏的进出事件历史来源为 fence 的考勤记录
Get fence entry/exit events (attendance records with source=fence).
"""
fence = await fence_service.get_fence(db, fence_id)
if fence is None:
raise HTTPException(status_code=404, detail="Fence not found")
from sqlalchemy.dialects.sqlite import JSON as _JSON
filters = [
AttendanceRecord.attendance_source == "fence",
]
if start_time:
filters.append(AttendanceRecord.recorded_at >= start_time)
if end_time:
filters.append(AttendanceRecord.recorded_at <= end_time)
# Filter by fence_id in lbs_data JSON (fence_auto records store fence_id there)
# Since SQLite JSON support is limited, we use LIKE for the fence_id match
filters.append(AttendanceRecord.lbs_data.like(f'%"fence_id": {fence_id}%'))
count_q = select(func.count(AttendanceRecord.id)).where(*filters)
total = (await db.execute(count_q)).scalar() or 0
offset = (page - 1) * page_size
result = await db.execute(
select(AttendanceRecord)
.where(*filters)
.order_by(AttendanceRecord.recorded_at.desc())
.offset(offset).limit(page_size)
)
records = result.scalars().all()
items = []
for r in records:
items.append({
"id": r.id,
"device_id": r.device_id,
"imei": r.imei,
"event_type": r.attendance_type,
"latitude": r.latitude,
"longitude": r.longitude,
"address": r.address,
"recorded_at": str(r.recorded_at),
})
return APIResponse(data=PaginatedList(
items=items,
total=total, page=page, page_size=page_size,
total_pages=math.ceil(total / page_size) if total else 0,
))
# ---------------------------------------------------------------------------
# Device-Fence Binding endpoints
# ---------------------------------------------------------------------------
@router.get(
"/{fence_id}/devices",
response_model=APIResponse[list[FenceDeviceDetail]],
summary="获取围栏绑定的设备列表",
)
async def get_fence_devices(fence_id: int, db: AsyncSession = Depends(get_db)):
fence = await fence_service.get_fence(db, fence_id)
if fence is None:
raise HTTPException(status_code=404, detail="Fence not found")
items = await fence_service.get_fence_devices(db, fence_id)
return APIResponse(data=[FenceDeviceDetail(**item) for item in items])
@router.post(
"/{fence_id}/devices",
response_model=APIResponse,
dependencies=[Depends(require_write)],
summary="绑定设备到围栏",
)
async def bind_devices(fence_id: int, body: DeviceFenceBindRequest, db: AsyncSession = Depends(get_db)):
result = await fence_service.bind_devices_to_fence(db, fence_id, body.device_ids)
if result.get("error"):
raise HTTPException(status_code=404, detail=result["error"])
return APIResponse(
message=f"绑定完成: 新增{result['created']}, 已绑定{result['already_bound']}, 未找到{result['not_found']}",
data=result,
)
@router.delete(
"/{fence_id}/devices",
response_model=APIResponse,
dependencies=[Depends(require_write)],
summary="解绑设备与围栏",
)
async def unbind_devices(fence_id: int, body: DeviceFenceBindRequest, db: AsyncSession = Depends(get_db)):
count = await fence_service.unbind_devices_from_fence(db, fence_id, body.device_ids)
return APIResponse(message=f"已解绑 {count} 个设备")