# ============================================================================= # 企微IT智能服务台 — 终端绑定管理 CRUD 路由 # ============================================================================= # 说明:管理后台用于管理小鱼易联终端与企微会议室的绑定关系 # # API 端点列表: # GET /itportal/admin/terminal-bindings — 绑定列表(分页+搜索) # POST /itportal/admin/terminal-bindings — 新增绑定 # PUT /itportal/admin/terminal-bindings/{id} — 更新绑定 # DELETE /itportal/admin/terminal-bindings/{id} — 删除绑定 # ============================================================================= import logging from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy import func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import get_current_user, UserInfo, require_admin from app.models.terminal_room_binding import TerminalRoomBinding from app.schemas.meetingroom import ( TerminalBindingCreate, TerminalBindingListResponse, TerminalBindingResponse, TerminalBindingUpdate, ) from app.utils.response import AppException, success_response logger = logging.getLogger(__name__) # 创建路由器 router = APIRouter(prefix="/itportal/admin/terminal-bindings", tags=["终端绑定管理"]) def _binding_to_response(binding: TerminalRoomBinding) -> dict: """将 TerminalRoomBinding ORM 对象转换为响应字典。 Args: binding: TerminalRoomBinding ORM 对象 Returns: dict: 响应格式字典 """ return TerminalBindingResponse( id=binding.id, terminal_sn=binding.terminal_sn, terminal_name=binding.terminal_name, meetingroom_id=binding.meetingroom_id, meetingroom_name=binding.meetingroom_name, location=binding.location, is_active=binding.is_active, created_at=binding.created_at.isoformat() if binding.created_at else "", updated_at=binding.updated_at.isoformat() if binding.updated_at else "", ).model_dump() # ============================================================================= # GET /itportal/admin/terminal-bindings — 绑定列表 # ============================================================================= @router.get("", response_model=None) @router.get("/", response_model=None) @require_admin async def list_terminal_bindings( page: int = Query(1, ge=1, description="页码(从1开始)"), size: int = Query(20, ge=1, le=100, description="每页条数"), keyword: Optional[str] = Query(None, description="搜索关键词(终端SN/名称/会议室名称)"), db: AsyncSession = Depends(get_db), current_user: UserInfo = Depends(get_current_user), ): """获取终端绑定列表(分页+搜索)。 需要管理员权限。 Args: page: 页码(从1开始) size: 每页条数 keyword: 搜索关键词(模糊匹配终端SN/终端名称/会议室名称) db: 数据库会话 current_user: 当前登录用户 Returns: Dict: 统一响应格式,data 含 list 和 total """ try: # 构建查询条件 conditions = [] if keyword: conditions.append( or_( TerminalRoomBinding.terminal_sn.ilike(f"%{keyword}%"), TerminalRoomBinding.terminal_name.ilike(f"%{keyword}%"), TerminalRoomBinding.meetingroom_name.ilike(f"%{keyword}%"), ) ) # 查询总数 count_stmt = select(func.count()).select_from(TerminalRoomBinding) if conditions: count_stmt = count_stmt.where(*conditions) count_result = await db.execute(count_stmt) total = count_result.scalar() or 0 # 查询列表(分页) stmt = select(TerminalRoomBinding).order_by(TerminalRoomBinding.id.desc()) if conditions: stmt = stmt.where(*conditions) stmt = stmt.offset((page - 1) * size).limit(size) result = await db.execute(stmt) bindings = result.scalars().all() # 转换为响应格式 binding_list = [_binding_to_response(b) for b in bindings] return success_response(data={ "list": binding_list, "total": total, }) except Exception as e: logger.error(f"查询终端绑定列表异常: {e}", exc_info=True) raise AppException(1005, f"查询终端绑定列表失败: {str(e)}") # ============================================================================= # POST /itportal/admin/terminal-bindings — 新增绑定 # ============================================================================= @router.post("", response_model=None) @router.post("/", response_model=None) @require_admin async def create_terminal_binding( body: TerminalBindingCreate, db: AsyncSession = Depends(get_db), current_user: UserInfo = Depends(get_current_user), ): """新增终端绑定。 需要管理员权限。 一个终端SN只能绑定一个会议室(terminal_sn 唯一约束)。 Args: body: 新增绑定请求 db: 数据库会话 current_user: 当前登录用户 Returns: Dict: 统一响应格式,data 含新建的绑定ID """ try: # 检查 terminal_sn 是否已存在 existing_stmt = select(TerminalRoomBinding).where( TerminalRoomBinding.terminal_sn == body.terminal_sn ) existing_result = await db.execute(existing_stmt) existing = existing_result.scalar_one_or_none() if existing: raise AppException(3107, f"终端SN '{body.terminal_sn}' 已绑定会议室,请先删除原有绑定") # 创建新绑定 binding = TerminalRoomBinding( terminal_sn=body.terminal_sn, terminal_name=body.terminal_name, meetingroom_id=body.meetingroom_id, meetingroom_name=body.meetingroom_name, location=body.location, is_active=True, ) db.add(binding) await db.flush() # 获取自增ID logger.info( f"新增终端绑定: sn={body.terminal_sn}, room_id={body.meetingroom_id}, " f"operator={current_user.employee_id}" ) return success_response(data={"id": binding.id}) except AppException: raise except Exception as e: logger.error(f"新增终端绑定异常: {e}", exc_info=True) raise AppException(1005, f"新增终端绑定失败: {str(e)}") # ============================================================================= # PUT /itportal/admin/terminal-bindings/{id} — 更新绑定 # ============================================================================= @router.put("/{binding_id}", response_model=None) @require_admin async def update_terminal_binding( binding_id: int, body: TerminalBindingUpdate, db: AsyncSession = Depends(get_db), current_user: UserInfo = Depends(get_current_user), ): """更新终端绑定信息。 需要管理员权限。仅更新传入的字段(部分更新)。 Args: binding_id: 绑定记录ID body: 更新请求 db: 数据库会话 current_user: 当前登录用户 Returns: Dict: 统一响应格式 """ try: stmt = select(TerminalRoomBinding).where(TerminalRoomBinding.id == binding_id) result = await db.execute(stmt) binding = result.scalar_one_or_none() if not binding: raise AppException(3107, "终端绑定不存在") # 部分更新(仅更新传入的字段) if body.terminal_name is not None: binding.terminal_name = body.terminal_name if body.meetingroom_id is not None: binding.meetingroom_id = body.meetingroom_id if body.meetingroom_name is not None: binding.meetingroom_name = body.meetingroom_name if body.location is not None: binding.location = body.location if body.is_active is not None: binding.is_active = body.is_active logger.info( f"更新终端绑定: id={binding_id}, operator={current_user.employee_id}" ) return success_response(data={}) except AppException: raise except Exception as e: logger.error(f"更新终端绑定异常: id={binding_id}, error={e}", exc_info=True) raise AppException(1005, f"更新终端绑定失败: {str(e)}") # ============================================================================= # DELETE /itportal/admin/terminal-bindings/{id} — 删除绑定 # ============================================================================= @router.delete("/{binding_id}", response_model=None) @require_admin async def delete_terminal_binding( binding_id: int, db: AsyncSession = Depends(get_db), current_user: UserInfo = Depends(get_current_user), ): """删除终端绑定。 需要管理员权限。 Args: binding_id: 绑定记录ID db: 数据库会话 current_user: 当前登录用户 Returns: Dict: 统一响应格式 """ try: stmt = select(TerminalRoomBinding).where(TerminalRoomBinding.id == binding_id) result = await db.execute(stmt) binding = result.scalar_one_or_none() if not binding: raise AppException(3107, "终端绑定不存在") await db.delete(binding) logger.info( f"删除终端绑定: id={binding_id}, sn={binding.terminal_sn}, " f"operator={current_user.employee_id}" ) return success_response(data={}) except AppException: raise except Exception as e: logger.error(f"删除终端绑定异常: id={binding_id}, error={e}", exc_info=True) raise AppException(1005, f"删除终端绑定失败: {str(e)}")