feat: 位置追踪优化、考勤去重、围栏考勤补充设备信息

- 地图轨迹点按定位类型区分颜色 (GPS蓝/WiFi青/LBS橙/蓝牙紫)
- LBS/WiFi定位点显示精度圈 (虚线圆, LBS~1km/WiFi~80m)
- 地图图例显示各定位类型颜色和精度范围
- 精度圈添加 bubble:true 防止遮挡轨迹点点击
- 点击列表记录直接在地图显示Marker+弹窗 (无需先加载轨迹)
- 修复3D地图setZoomAndCenter坐标偏移, 改用setCenter+setZoom
- 最新位置轮询超时从15s延长至30s (适配LBS慢响应)
- 考勤每日去重: 同设备同类型每天只记录一条 (fence/device/bluetooth通用)
- 围栏自动考勤补充设备电量/信号/基站信息 (从Device表和位置包获取)
- 考勤来源字段 attendance_source 区分 device/bluetooth/fence

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
2026-03-30 04:26:29 +00:00
parent 1d06cc5415
commit 891344bfa0
8 changed files with 598 additions and 100 deletions

View File

@@ -7,7 +7,7 @@ import math
from datetime import datetime
from fastapi import APIRouter, Body, Depends, HTTPException, Query
from sqlalchemy import select, delete
from sqlalchemy import func, select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import require_write
@@ -140,6 +140,68 @@ async def device_track(
)
@router.post(
"/batch-delete",
response_model=APIResponse[dict],
summary="批量删除位置记录 / Batch delete location records",
dependencies=[Depends(require_write)],
)
async def batch_delete_locations(
location_ids: list[int] = Body(..., min_length=1, max_length=500, embed=True),
db: AsyncSession = Depends(get_db),
):
"""批量删除位置记录最多500条"""
result = await db.execute(
delete(LocationRecord).where(LocationRecord.id.in_(location_ids))
)
await db.flush()
return APIResponse(
message=f"已删除 {result.rowcount} 条位置记录",
data={"deleted": result.rowcount, "requested": len(location_ids)},
)
@router.post(
"/delete-no-coords",
response_model=APIResponse[dict],
summary="删除无坐标位置记录 / Delete location records without coordinates",
dependencies=[Depends(require_write)],
)
async def delete_no_coord_locations(
device_id: int | None = Body(default=None, description="设备ID (可选,不传则所有设备)"),
start_time: str | None = Body(default=None, description="开始时间 ISO 8601"),
end_time: str | None = Body(default=None, description="结束时间 ISO 8601"),
db: AsyncSession = Depends(get_db),
):
"""删除经纬度为空的位置记录,可按设备和时间范围过滤。"""
from datetime import datetime as dt
conditions = [
(LocationRecord.latitude.is_(None)) | (LocationRecord.longitude.is_(None))
]
if device_id is not None:
conditions.append(LocationRecord.device_id == device_id)
if start_time:
conditions.append(LocationRecord.recorded_at >= dt.fromisoformat(start_time))
if end_time:
conditions.append(LocationRecord.recorded_at <= dt.fromisoformat(end_time))
# Count first
count_result = await db.execute(
select(func.count(LocationRecord.id)).where(*conditions)
)
count = count_result.scalar() or 0
if count > 0:
await db.execute(delete(LocationRecord).where(*conditions))
await db.flush()
return APIResponse(
message=f"已删除 {count} 条无坐标记录",
data={"deleted": count},
)
@router.get(
"/{location_id}",
response_model=APIResponse[LocationRecordResponse],