✨ feat(vscode): 代码阅读加 FastAPI 接口卡片:请求表(URL、方法、路径 / 查询参数、请求头、Cookie、请求体)、按状态码的响应表(成功、responses、函数里抛的异常、自动带的 422);别的文件里的异常状态码、类型别名经工作区索引和语言扩展查
This commit is contained in:
1 parent
1dfc0c7b1e
commit
0798f7d1c7
18 files changed
+1745
-28
No files matched your search
@@ -0,0 +1,194 @@
|
||||
"""用户管理 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)
|
||||
|
||||
Reference in new issue
Block a user