- 前向地理编码升级为高德IoT v5 API (POST restapi.amap.com/v5/position/IoT) - 修复LBS定位偏差: 添加network=LTE参数区分4G/2G, bts格式补充cage字段 - 新增电子围栏管理模块 (circle/polygon/rectangle), 支持地图绘制和POI搜索 - 新增设备-围栏多对多绑定 (DeviceFenceBinding/DeviceFenceState) - 围栏自动考勤引擎 (fence_checker.py): haversine距离、ray-casting多边形判定、容差机制、防抖 - TCP位置上报自动检测围栏进出, 生成考勤记录并WebSocket广播 - 前端围栏页面: 绑定设备弹窗、POI搜索定位、左侧围栏面板 - 新增fence_attendance WebSocket topic via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
120 lines
4.7 KiB
Python
120 lines
4.7 KiB
Python
"""Fences Router - geofence management API endpoints."""
|
|
|
|
import math
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.dependencies import require_write
|
|
from app.database import get_db
|
|
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("/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")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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} 个设备")
|