Files
desungongpai/app/routers/device_groups.py

211 lines
6.9 KiB
Python
Raw Permalink Normal View History

"""
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},
)