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:
@@ -30,6 +30,7 @@ router = APIRouter(prefix="/api/attendance", tags=["Attendance / 考勤管理"])
|
||||
async def list_attendance(
|
||||
device_id: int | None = Query(default=None, description="设备ID / Device ID"),
|
||||
attendance_type: str | None = Query(default=None, description="考勤类型 / Attendance type"),
|
||||
attendance_source: str | None = Query(default=None, description="考勤来源 / Source (device/bluetooth/fence)"),
|
||||
start_time: datetime | None = Query(default=None, description="开始时间 / Start time (ISO 8601)"),
|
||||
end_time: datetime | None = Query(default=None, description="结束时间 / End time (ISO 8601)"),
|
||||
page: int = Query(default=1, ge=1, description="页码 / Page number"),
|
||||
@@ -37,8 +38,8 @@ async def list_attendance(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
获取考勤记录列表,支持按设备、考勤类型、时间范围过滤。
|
||||
List attendance records with filters for device, type, and time range.
|
||||
获取考勤记录列表,支持按设备、考勤类型、来源、时间范围过滤。
|
||||
List attendance records with filters for device, type, source, and time range.
|
||||
"""
|
||||
query = select(AttendanceRecord)
|
||||
count_query = select(func.count(AttendanceRecord.id))
|
||||
@@ -51,6 +52,10 @@ async def list_attendance(
|
||||
query = query.where(AttendanceRecord.attendance_type == attendance_type)
|
||||
count_query = count_query.where(AttendanceRecord.attendance_type == attendance_type)
|
||||
|
||||
if attendance_source:
|
||||
query = query.where(AttendanceRecord.attendance_source == attendance_source)
|
||||
count_query = count_query.where(AttendanceRecord.attendance_source == attendance_source)
|
||||
|
||||
if start_time:
|
||||
query = query.where(AttendanceRecord.recorded_at >= start_time)
|
||||
count_query = count_query.where(AttendanceRecord.recorded_at >= start_time)
|
||||
|
||||
@@ -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