""" Device Groups Router - 设备分组管理接口 API endpoints for device group CRUD and membership management. """ from fastapi import APIRouter, Body, Depends, HTTPException, Query from pydantic import BaseModel, Field from sqlalchemy import func, select, delete from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import require_write from app.models import Device, DeviceGroup, DeviceGroupMember from app.schemas import ( APIResponse, DeviceGroupCreate, DeviceGroupResponse, DeviceGroupUpdate, DeviceGroupWithCount, DeviceResponse, ) router = APIRouter(prefix="/api/groups", tags=["Device Groups / 设备分组"]) @router.get( "", response_model=APIResponse[list[DeviceGroupWithCount]], summary="获取分组列表 / List device groups", ) async def list_groups(db: AsyncSession = Depends(get_db)): """获取所有设备分组及各组设备数。""" result = await db.execute( select( DeviceGroup, func.count(DeviceGroupMember.id).label("cnt"), ) .outerjoin(DeviceGroupMember, DeviceGroup.id == DeviceGroupMember.group_id) .group_by(DeviceGroup.id) .order_by(DeviceGroup.name) ) groups = [] for row in result.all(): g = DeviceGroupResponse.model_validate(row[0]) groups.append(DeviceGroupWithCount(**g.model_dump(), device_count=row[1])) return APIResponse(data=groups) @router.post( "", response_model=APIResponse[DeviceGroupResponse], status_code=201, summary="创建分组 / Create group", dependencies=[Depends(require_write)], ) async def create_group(body: DeviceGroupCreate, db: AsyncSession = Depends(get_db)): """创建新设备分组。""" existing = await db.execute( select(DeviceGroup).where(DeviceGroup.name == body.name) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail=f"分组名 '{body.name}' 已存在") group = DeviceGroup(name=body.name, description=body.description, color=body.color) db.add(group) await db.flush() await db.refresh(group) return APIResponse(data=DeviceGroupResponse.model_validate(group)) @router.put( "/{group_id}", response_model=APIResponse[DeviceGroupResponse], summary="更新分组 / Update group", dependencies=[Depends(require_write)], ) async def update_group( group_id: int, body: DeviceGroupUpdate, db: AsyncSession = Depends(get_db) ): """更新分组信息。""" result = await db.execute(select(DeviceGroup).where(DeviceGroup.id == group_id)) group = result.scalar_one_or_none() if not group: raise HTTPException(status_code=404, detail=f"分组 {group_id} 不存在") update_data = body.model_dump(exclude_unset=True) for k, v in update_data.items(): setattr(group, k, v) await db.flush() await db.refresh(group) return APIResponse(data=DeviceGroupResponse.model_validate(group)) @router.delete( "/{group_id}", response_model=APIResponse, summary="删除分组 / Delete group", dependencies=[Depends(require_write)], ) async def delete_group(group_id: int, db: AsyncSession = Depends(get_db)): """删除分组(级联删除成员关系,不删除设备)。""" result = await db.execute(select(DeviceGroup).where(DeviceGroup.id == group_id)) group = result.scalar_one_or_none() if not group: raise HTTPException(status_code=404, detail=f"分组 {group_id} 不存在") # Clear group_id on devices devices_result = await db.execute(select(Device).where(Device.group_id == group_id)) for d in devices_result.scalars().all(): d.group_id = None await db.delete(group) await db.flush() return APIResponse(message="分组已删除") @router.get( "/{group_id}/devices", response_model=APIResponse[list[DeviceResponse]], summary="获取分组设备 / Get group devices", ) async def get_group_devices(group_id: int, db: AsyncSession = Depends(get_db)): """获取分组内的设备列表。""" result = await db.execute( select(Device) .join(DeviceGroupMember, Device.id == DeviceGroupMember.device_id) .where(DeviceGroupMember.group_id == group_id) .order_by(Device.name) ) devices = list(result.scalars().all()) return APIResponse(data=[DeviceResponse.model_validate(d) for d in devices]) class GroupMemberRequest(BaseModel): device_ids: list[int] = Field(..., min_length=1, max_length=500, description="设备ID列表") @router.post( "/{group_id}/devices", response_model=APIResponse[dict], summary="添加设备到分组 / Add devices to group", dependencies=[Depends(require_write)], ) async def add_devices_to_group( group_id: int, body: GroupMemberRequest, db: AsyncSession = Depends(get_db) ): """添加设备到分组(幂等,已存在的跳过)。""" # Verify group exists group = (await db.execute( select(DeviceGroup).where(DeviceGroup.id == group_id) )).scalar_one_or_none() if not group: raise HTTPException(status_code=404, detail=f"分组 {group_id} 不存在") # Get existing members existing = await db.execute( select(DeviceGroupMember.device_id).where( DeviceGroupMember.group_id == group_id, DeviceGroupMember.device_id.in_(body.device_ids), ) ) existing_ids = {row[0] for row in existing.all()} added = 0 for did in body.device_ids: if did not in existing_ids: db.add(DeviceGroupMember(device_id=did, group_id=group_id)) added += 1 # Also update device.group_id if body.device_ids: devices = await db.execute( select(Device).where(Device.id.in_(body.device_ids)) ) for d in devices.scalars().all(): d.group_id = group_id await db.flush() return APIResponse( message=f"已添加 {added} 台设备到分组", data={"added": added, "already_in_group": len(existing_ids)}, ) @router.delete( "/{group_id}/devices", response_model=APIResponse[dict], summary="从分组移除设备 / Remove devices from group", dependencies=[Depends(require_write)], ) async def remove_devices_from_group( group_id: int, body: GroupMemberRequest, db: AsyncSession = Depends(get_db) ): """从分组移除设备。""" result = await db.execute( delete(DeviceGroupMember).where( DeviceGroupMember.group_id == group_id, DeviceGroupMember.device_id.in_(body.device_ids), ) ) # Clear group_id on removed devices devices = await db.execute( select(Device).where( Device.id.in_(body.device_ids), Device.group_id == group_id, ) ) for d in devices.scalars().all(): d.group_id = None await db.flush() return APIResponse( message=f"已移除 {result.rowcount} 台设备", data={"removed": result.rowcount}, )