feat: 高德IoT v5 API升级、电子围栏管理、设备绑定自动考勤

- 前向地理编码升级为高德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>
This commit is contained in:
2026-03-27 13:04:11 +00:00
parent cde5146bfe
commit 1d06cc5415
17 changed files with 2303 additions and 187 deletions

View File

@@ -13,7 +13,7 @@ from slowapi.errors import RateLimitExceeded
from app.database import init_db, async_session, engine
from app.tcp_server import tcp_manager
from app.config import settings
from app.routers import devices, locations, alarms, attendance, commands, bluetooth, beacons, heartbeats, api_keys, ws, geocoding
from app.routers import devices, locations, alarms, attendance, commands, bluetooth, beacons, fences, heartbeats, api_keys, ws, geocoding
from app.dependencies import verify_api_key, require_write, require_admin
import asyncio
@@ -28,11 +28,12 @@ from app.extensions import limiter
async def run_data_cleanup():
"""Delete records older than DATA_RETENTION_DAYS."""
from datetime import datetime, timezone, timedelta
from datetime import timedelta
from sqlalchemy import delete
from app.models import LocationRecord, HeartbeatRecord, AlarmRecord, AttendanceRecord, BluetoothRecord
from app.config import now_cst
cutoff = datetime.now(timezone.utc) - timedelta(days=settings.DATA_RETENTION_DAYS)
cutoff = now_cst() - timedelta(days=settings.DATA_RETENTION_DAYS)
total_deleted = 0
async with async_session() as session:
async with session.begin():
@@ -176,6 +177,7 @@ app.include_router(attendance.router, dependencies=[*_api_deps])
app.include_router(commands.router, dependencies=[*_api_deps])
app.include_router(bluetooth.router, dependencies=[*_api_deps])
app.include_router(beacons.router, dependencies=[*_api_deps])
app.include_router(fences.router, dependencies=[*_api_deps])
app.include_router(heartbeats.router, dependencies=[*_api_deps])
app.include_router(api_keys.router, dependencies=[*_api_deps])
app.include_router(ws.router) # WebSocket handles auth internally