Files
desungongpai/app/routers/beacons.py
default 9cd9dd9d76 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>
2026-04-01 07:06:37 +00:00

226 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Beacons Router - 蓝牙信标管理接口
API endpoints for managing Bluetooth beacon configuration.
"""
import math
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import require_write
from app.database import get_db
from app.schemas import (
APIResponse,
BeaconConfigCreate,
BeaconConfigResponse,
BeaconConfigUpdate,
BeaconDeviceDetail,
DeviceBeaconBindRequest,
PaginatedList,
)
from app.services import beacon_service
router = APIRouter(prefix="/api/beacons", tags=["Beacons / 蓝牙信标"])
@router.get(
"",
response_model=APIResponse[PaginatedList[BeaconConfigResponse]],
summary="获取信标列表 / List beacons",
)
async def list_beacons(
status: str | None = Query(default=None, description="状态筛选 (active/inactive)"),
search: str | None = Query(default=None, description="搜索 MAC/名称/区域"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
):
records, total = await beacon_service.get_beacons(
db, page=page, page_size=page_size, status_filter=status, search=search
)
return APIResponse(
data=PaginatedList(
items=[BeaconConfigResponse.model_validate(r) for r in records],
total=total,
page=page,
page_size=page_size,
total_pages=math.ceil(total / page_size) if total else 0,
)
)
@router.post(
"/setup-bluetooth-mode",
response_model=APIResponse,
summary="批量配置蓝牙打卡模式 / Setup BT clock-in mode for devices",
dependencies=[Depends(require_write)],
)
async def setup_bluetooth_mode(
device_ids: list[int] | None = Query(default=None, description="指定设备ID不传则所有在线设备"),
db: AsyncSession = Depends(get_db),
):
result = await beacon_service.setup_bluetooth_mode(db, device_ids)
if result["error"]:
return APIResponse(code=1, message=result["error"], data=result)
return APIResponse(
message=f"{result['total']} 台: {result['sent']} 台已配置, {result['failed']} 台失败",
data=result,
)
@router.post(
"/restore-normal-mode",
response_model=APIResponse,
summary="恢复正常模式 / Restore devices to normal (smart) mode",
dependencies=[Depends(require_write)],
)
async def restore_normal_mode(
device_ids: list[int] | None = Query(default=None, description="指定设备ID不传则所有在线设备"),
db: AsyncSession = Depends(get_db),
):
result = await beacon_service.restore_normal_mode(db, device_ids)
if result["error"]:
return APIResponse(code=1, message=result["error"], data=result)
return APIResponse(
message=f"{result['total']} 台: {result['sent']} 台已恢复, {result['failed']} 台失败",
data=result,
)
@router.post(
"/reverse-sync",
response_model=APIResponse,
summary="从设备反向同步信标配置 / Query devices and update DB bindings",
dependencies=[Depends(require_write)],
)
async def reverse_sync_beacons(db: AsyncSession = Depends(get_db)):
result = await beacon_service.reverse_sync_from_devices(db)
if result["error"]:
return APIResponse(code=1, message=result["error"], data=result)
return APIResponse(
message=f"查询 {result['queried']} 台设备,{result['responded']} 台响应,{result['updated']} 台有变更",
data=result,
)
@router.post(
"/sync-device/{device_id}",
response_model=APIResponse,
summary="同步信标配置到设备 / Sync beacon MACs to device via BTMACSET",
dependencies=[Depends(require_write)],
)
async def sync_device_beacons(device_id: int, db: AsyncSession = Depends(get_db)):
result = await beacon_service.sync_device_beacons(db, device_id)
if result["error"]:
return APIResponse(code=1, message=result["error"], data=result)
return APIResponse(
message=f"已发送 {len(result['commands'])} 条指令,共 {result['mac_count']} 个信标MAC",
data=result,
)
@router.get(
"/{beacon_id}",
response_model=APIResponse[BeaconConfigResponse],
summary="获取信标详情 / Get beacon",
)
async def get_beacon(beacon_id: int, db: AsyncSession = Depends(get_db)):
beacon = await beacon_service.get_beacon(db, beacon_id)
if beacon is None:
raise HTTPException(status_code=404, detail="Beacon not found")
return APIResponse(data=BeaconConfigResponse.model_validate(beacon))
@router.post(
"",
response_model=APIResponse[BeaconConfigResponse],
status_code=201,
summary="添加信标 / Create beacon",
dependencies=[Depends(require_write)],
)
async def create_beacon(body: BeaconConfigCreate, db: AsyncSession = Depends(get_db)):
existing = await beacon_service.get_beacon_by_mac(db, body.beacon_mac)
if existing:
raise HTTPException(status_code=400, detail=f"Beacon MAC {body.beacon_mac} already exists")
beacon = await beacon_service.create_beacon(db, body)
return APIResponse(message="Beacon created", data=BeaconConfigResponse.model_validate(beacon))
@router.put(
"/{beacon_id}",
response_model=APIResponse[BeaconConfigResponse],
summary="更新信标 / Update beacon",
dependencies=[Depends(require_write)],
)
async def update_beacon(
beacon_id: int, body: BeaconConfigUpdate, db: AsyncSession = Depends(get_db)
):
beacon = await beacon_service.update_beacon(db, beacon_id, body)
if beacon is None:
raise HTTPException(status_code=404, detail="Beacon not found")
return APIResponse(message="Beacon updated", data=BeaconConfigResponse.model_validate(beacon))
@router.delete(
"/{beacon_id}",
response_model=APIResponse,
summary="删除信标 / Delete beacon",
dependencies=[Depends(require_write)],
)
async def delete_beacon(beacon_id: int, db: AsyncSession = Depends(get_db)):
success = await beacon_service.delete_beacon(db, beacon_id)
if not success:
raise HTTPException(status_code=404, detail="Beacon not found")
return APIResponse(message="Beacon deleted")
# ---------------------------------------------------------------------------
# Device-Beacon Binding endpoints
# ---------------------------------------------------------------------------
@router.get(
"/{beacon_id}/devices",
response_model=APIResponse[list[BeaconDeviceDetail]],
summary="获取信标绑定的设备 / Get beacon devices",
)
async def get_beacon_devices(beacon_id: int, db: AsyncSession = Depends(get_db)):
beacon = await beacon_service.get_beacon(db, beacon_id)
if beacon is None:
raise HTTPException(status_code=404, detail="Beacon not found")
items = await beacon_service.get_beacon_devices(db, beacon_id)
return APIResponse(data=[BeaconDeviceDetail(**item) for item in items])
@router.post(
"/{beacon_id}/devices",
response_model=APIResponse,
summary="绑定设备到信标 / Bind devices to beacon",
dependencies=[Depends(require_write)],
)
async def bind_devices(
beacon_id: int, body: DeviceBeaconBindRequest, db: AsyncSession = Depends(get_db),
):
result = await beacon_service.bind_devices_to_beacon(db, beacon_id, body.device_ids)
if result.get("error"):
raise HTTPException(status_code=404, detail=result["error"])
return APIResponse(
message=f"绑定完成: 新增{result['created']}, 已绑定{result['already_bound']}, 未找到{result['not_found']}",
data=result,
)
@router.delete(
"/{beacon_id}/devices",
response_model=APIResponse,
summary="解绑设备 / Unbind devices from beacon",
dependencies=[Depends(require_write)],
)
async def unbind_devices(
beacon_id: int, body: DeviceBeaconBindRequest, db: AsyncSession = Depends(get_db),
):
count = await beacon_service.unbind_devices_from_beacon(db, beacon_id, body.device_ids)
return APIResponse(message=f"已解绑 {count} 个设备")