feat: 信标设备绑定 + 蓝牙模式管理 + 系统管理增强 + 数据导出
- 新增 DeviceBeaconBinding 模型,信标-设备多对多绑定 CRUD - 蓝牙打卡模式批量配置/恢复正常模式 API - 反向同步: 查询设备 BTMACSET 配置更新数据库绑定 (独立 session 解决事务隔离) - 设备列表快捷操作弹窗修复 (fire-and-forget IIFE 替代阻塞轮询) - 保存按钮防抖: 围栏/信标绑定保存点击后 disabled + 转圈防重复提交 - 审计日志中间件 + 系统配置/备份/固件 API - 设备分组管理 + 告警规则配置 - 5个数据导出 API (CSV UTF-8 BOM) - 位置热力图 + 告警条件删除 + 位置清理 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -8,11 +8,13 @@ from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import BluetoothRecord
|
||||
from app.services.export_utils import build_csv_content, csv_filename
|
||||
from app.schemas import (
|
||||
APIResponse,
|
||||
BluetoothRecordResponse,
|
||||
@@ -86,6 +88,46 @@ async def list_bluetooth_records(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/export",
|
||||
summary="导出蓝牙记录 CSV / Export bluetooth records CSV",
|
||||
)
|
||||
async def export_bluetooth(
|
||||
device_id: int | None = Query(default=None, description="设备ID"),
|
||||
record_type: str | None = Query(default=None, description="记录类型 (punch/location)"),
|
||||
beacon_mac: str | None = Query(default=None, description="信标MAC"),
|
||||
start_time: datetime | None = Query(default=None, description="开始时间 ISO 8601"),
|
||||
end_time: datetime | None = Query(default=None, description="结束时间 ISO 8601"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导出蓝牙记录为 CSV,支持设备/类型/信标/时间筛选。最多导出 50000 条。"""
|
||||
query = select(BluetoothRecord)
|
||||
if device_id is not None:
|
||||
query = query.where(BluetoothRecord.device_id == device_id)
|
||||
if record_type:
|
||||
query = query.where(BluetoothRecord.record_type == record_type)
|
||||
if beacon_mac:
|
||||
query = query.where(BluetoothRecord.beacon_mac == beacon_mac)
|
||||
if start_time:
|
||||
query = query.where(BluetoothRecord.recorded_at >= start_time)
|
||||
if end_time:
|
||||
query = query.where(BluetoothRecord.recorded_at <= end_time)
|
||||
query = query.order_by(BluetoothRecord.recorded_at.desc()).limit(50000)
|
||||
|
||||
result = await db.execute(query)
|
||||
records = list(result.scalars().all())
|
||||
|
||||
headers = ["ID", "设备ID", "IMEI", "记录类型", "信标MAC", "UUID", "Major", "Minor", "RSSI", "电量", "考勤类型", "纬度", "经度", "记录时间"]
|
||||
fields = ["id", "device_id", "imei", "record_type", "beacon_mac", "beacon_uuid", "beacon_major", "beacon_minor", "rssi", "beacon_battery", "attendance_type", "latitude", "longitude", "recorded_at"]
|
||||
|
||||
content = build_csv_content(headers, records, fields)
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": f"attachment; filename={csv_filename('bluetooth')}"},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/stats",
|
||||
response_model=APIResponse[dict],
|
||||
|
||||
Reference in New Issue
Block a user