feat: 围栏Tab布局重构、低精度过滤、蓝牙考勤去重、考勤删除API

- 围栏管理页面Tab移至顶部,设备绑定Tab隐藏地图全屏展示绑定矩阵
- 位置追踪新增"低精度"按钮,隐藏LBS/WiFi点(地图+折线+表格联动)
- 移除LBS/WiFi精度半径圆圈,仅通过标记颜色区分定位类型
- 蓝牙打卡(0xB2)自动创建考勤记录,含去重和WebSocket广播
- 新增考勤批量删除和单条删除API
- fence_checker补充json导入

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

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
2026-03-30 09:41:55 +00:00
parent 891344bfa0
commit 3437cd24ea
5 changed files with 391 additions and 719 deletions

View File

@@ -192,6 +192,36 @@ async def device_attendance(
)
@router.post(
"/batch-delete",
response_model=APIResponse[dict],
summary="批量删除考勤记录 / Batch delete attendance records",
)
async def batch_delete_attendance(
body: dict,
db: AsyncSession = Depends(get_db),
):
"""
批量删除考勤记录,通过 body 传递 attendance_ids 列表,最多 500 条。
Batch delete attendance records by IDs (max 500).
"""
attendance_ids = body.get("attendance_ids", [])
if not attendance_ids:
raise HTTPException(status_code=400, detail="attendance_ids is required")
if len(attendance_ids) > 500:
raise HTTPException(status_code=400, detail="Max 500 records per request")
result = await db.execute(
select(AttendanceRecord).where(AttendanceRecord.id.in_(attendance_ids))
)
records = list(result.scalars().all())
for r in records:
await db.delete(r)
await db.flush()
return APIResponse(data={"deleted": len(records)})
# NOTE: /{attendance_id} must be after /stats and /device/{device_id} to avoid route conflicts
@router.get(
"/{attendance_id}",
@@ -207,3 +237,21 @@ async def get_attendance(attendance_id: int, db: AsyncSession = Depends(get_db))
if record is None:
raise HTTPException(status_code=404, detail=f"Attendance {attendance_id} not found")
return APIResponse(data=AttendanceRecordResponse.model_validate(record))
@router.delete(
"/{attendance_id}",
response_model=APIResponse[dict],
summary="删除单条考勤记录 / Delete attendance record",
)
async def delete_attendance(attendance_id: int, db: AsyncSession = Depends(get_db)):
"""按ID删除单条考勤记录 / Delete a single attendance record by ID."""
result = await db.execute(
select(AttendanceRecord).where(AttendanceRecord.id == attendance_id)
)
record = result.scalar_one_or_none()
if record is None:
raise HTTPException(status_code=404, detail=f"Attendance {attendance_id} not found")
await db.delete(record)
await db.flush()
return APIResponse(data={"deleted": 1})