""" 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} 个设备")