"""用户管理 console (admin) 路由。 提供给管理后台用的用户 CRUD、批量开号、批量删除与启用状态切换接口, 路径前缀 ``/admin/users``, 全部需要管理员权限。 """ from __future__ import annotations import uuid from typing import Annotated, Literal from fastapi import APIRouter, Depends, Query from langgraph.checkpoint.base import BaseCheckpointSaver from luffy_billing_sdk.client import BillingClient from sqlalchemy.ext.asyncio import AsyncSession from luffy_agent.acquisition.service import acquisition_service from luffy_agent.agent.runtime.deps import ( get_agent_pool, get_checkpointer, get_session_runner, ) from luffy_agent.agent.runtime.pool import AgentPool from luffy_agent.agent.runtime.runner import SessionRunner from luffy_agent.campaign.triggers import emit_user_registered from luffy_agent.core.db import get_db from luffy_agent.core.db.schemas import BulkDeleteFailedItem from luffy_agent.core.exceptions import Forbidden from luffy_agent.core.filter.deps import FilterQuery from luffy_agent.core.params import Int4Path from luffy_agent.deployment.deps import get_billing_client from luffy_agent.user.auth.deps import ( ConsolePrincipal, declares_permission, require_console_user, require_permission, ) from luffy_agent.user.console.rbac.service import ( bind_console_audit_request, console_rbac_service, ) from luffy_agent.user.console.schemas import ( ImpersonateRequest, ImpersonateResponse, UserBatchCreateItem, UserBatchCreateRequest, UserBatchCreateResponse, UserBulkDeleteRequest, UserBulkDeleteResponse, UserCreateRequest, UserListItem, UserListResponse, UserPhoneBackfillConflictItem, UserPhoneBackfillFilledItem, UserPhoneBackfillResponse, UserToggleActiveResponse, ) from luffy_agent.user.models import User, UserRole from luffy_agent.user.schemas import UserDetailResponse, UserUpdateRequest from luffy_agent.user.services import BatchUserResult, user_service router = APIRouter(tags=["用户管理-运维"], prefix="/admin/users") def _assert_permission(principal: ConsolePrincipal, key: str) -> None: if not principal.is_root and key not in principal.permissions: raise Forbidden("无操作权限") def _reject_admin_role(role: UserRole) -> None: if role == UserRole.ADMIN: raise Forbidden("不能设置管理员角色") def _to_detail(user: User) -> UserDetailResponse: return UserDetailResponse( id=user.id, username=user.username, display_name=user.display_name, role=user.role.value, email=user.email, phone=user.phone, bio=user.bio, avatar_url=user.avatar_url, is_active=user.is_active, created_at=user.created_at, updated_at=user.updated_at, ) @declares_permission("users:activate") async def require_toggle_active_permission( user_id: int, db: AsyncSession = Depends(get_db), principal: ConsolePrincipal = Depends(require_console_user), ) -> ConsolePrincipal: user = await user_service.get_user(db, user_id=user_id) key = "users:deactivate" if user.is_active else "users:activate" _assert_permission(principal, key) return principal require_users_view = require_permission("users:view") require_users_create = require_permission("users:create") require_users_batch_create = require_permission("users:batch_create") require_users_update = require_permission("users:update") require_users_delete = require_permission("users:delete") require_users_bulk_delete = require_permission("users:bulk_delete") require_users_impersonate = require_permission("users:impersonate") @router.get("", response_model=UserListResponse) async def list_users( filters: FilterQuery, page: Annotated[int, Query(ge=1, description="页码")] = 1, page_size: Annotated[int, Query(ge=1, le=100, description="每页数量")] = 20, acquisition_channel_id: Annotated[ int | None, Query(description="按来源渠道 ID 精确筛选") ] = None, referred: Annotated[ Literal["yes", "no"] | None, Query(description="按是否曾被邀请筛选: yes=曾受邀 / no=从未受邀"), ] = None, db: AsyncSession = Depends(get_db), _: object = Depends(require_users_view), ) -> UserListResponse: """获取用户列表(支持筛选)。 需要管理员权限。 """ skip = (page - 1) * page_size rows, total = await user_service.get_admin_users( db, skip=skip, limit=page_size, filters=filters.filters, acquisition_channel_id=acquisition_channel_id, referred=None if referred is None else referred == "yes", ) return UserListResponse( items=[ UserListItem( id=row.user.id, username=row.user.username, display_name=row.user.display_name, role=row.user.role.value, phone=row.user.phone, is_active=row.user.is_active, acquisition_channel_id=row.channel.id, acquisition_channel_code=row.channel.code, acquisition_channel_name=row.channel.name, referred=row.referred, created_at=row.user.created_at, updated_at=row.user.updated_at, ) for row in rows ], total=total, page=page, page_size=page_size, ) @router.post("", response_model=UserDetailResponse, status_code=201) async def create_user( body: UserCreateRequest, db: AsyncSession = Depends(get_db), _: object = Depends(require_users_create), ) -> UserDetailResponse: """创建新用户。 需要管理员权限。密码会自动哈希。 """ _reject_admin_role(body.role) admin_channel_id = await acquisition_service.resolve_registration_channel_id( db, created_via="password", channel_code=None, admin_created=True, ) user = await user_service.create_user( db, username=body.username, password=body.password, display_name=body.display_name, acquisition_channel_id=admin_channel_id, role=body.role, is_active=body.is_active, ) emit_user_registered(user.id) return _to_detail(user)