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:
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user