Files
Aether/_deprecated_py_src/api/admin/users/routes.py

1005 lines
34 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""用户管理 API 端点。"""
from __future__ import annotations
from typing import Any
2025-12-10 20:52:44 +08:00
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from fastapi.concurrency import run_in_threadpool
2025-12-10 20:52:44 +08:00
from pydantic import ValidationError
from sqlalchemy import func
from sqlalchemy.orm import Session
2025-12-10 20:52:44 +08:00
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import get_pipeline
from src.config.constants import CacheTTL
2025-12-10 20:52:44 +08:00
from src.core.exceptions import InvalidRequestException, NotFoundException, translate_pydantic_error
from src.core.logger import logger
from src.database import get_db, get_db_context
2025-12-10 20:52:44 +08:00
from src.models.admin_requests import UpdateUserRequest
from src.models.api import (
CreateApiKeyRequest,
CreateUserRequest,
UpdateMyApiKeyRequest,
UserSessionResponse,
)
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
from src.models.database import ApiKey, User, UserRole, Wallet
from src.services.auth.session_service import SessionService
from src.services.cache.user_cache import UserCacheService
from src.services.system.config import SystemConfigService
2025-12-10 20:52:44 +08:00
from src.services.user.apikey import ApiKeyService
from src.services.user.bulk_cleanup import pre_clean_api_key
2025-12-10 20:52:44 +08:00
from src.services.user.service import UserService
from src.services.wallet import WalletService
from src.utils.cache_decorator import cache_result
2025-12-10 20:52:44 +08:00
router = APIRouter(prefix="/api/admin/users", tags=["Admin - Users"])
pipeline = get_pipeline()
2025-12-10 20:52:44 +08:00
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
class _WalletSentinelType:
pass
_WALLET_SENTINEL = _WalletSentinelType()
def _serialize_user(
db: Session,
user: User,
wallet: Wallet | None | _WalletSentinelType = _WALLET_SENTINEL,
) -> dict[str, Any]:
resolved_wallet: Wallet | None
if wallet is _WALLET_SENTINEL:
resolved_wallet = WalletService.get_wallet(db, user_id=user.id)
else:
resolved_wallet = wallet
return {
"id": user.id,
"email": user.email,
"username": user.username,
"role": user.role.value,
"allowed_providers": user.allowed_providers,
"allowed_api_formats": user.allowed_api_formats,
"allowed_models": user.allowed_models,
"rate_limit": user.rate_limit,
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
"unlimited": WalletService.is_unlimited_wallet(resolved_wallet),
"is_active": user.is_active,
"created_at": user.created_at.isoformat(),
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
"last_login_at": user.last_login_at.isoformat() if user.last_login_at else None,
}
def _create_user_sync(
request: CreateUserRequest, role: UserRole
) -> tuple[dict[str, Any], dict[str, Any]]:
with get_db_context() as db:
if request.unlimited:
initial_gift_usd = None
elif request.initial_gift_usd is not None:
initial_gift_usd = request.initial_gift_usd
else:
initial_gift_usd = SystemConfigService.get_config(
db, "default_user_initial_gift_usd", default=None
)
user = UserService.create_user(
db=db,
email=request.email,
username=request.username,
password=request.password,
role=role,
initial_gift_usd=initial_gift_usd,
unlimited=request.unlimited,
allowed_providers=request.allowed_providers,
allowed_api_formats=request.allowed_api_formats,
allowed_models=request.allowed_models,
rate_limit=request.rate_limit,
)
return _serialize_user(db, user), {
"action": "create_user",
"target_user_id": user.id,
"target_email": user.email,
"target_username": user.username,
"target_role": user.role.value,
"initial_gift_usd": initial_gift_usd,
"unlimited": request.unlimited,
"is_active": user.is_active,
}
def _update_user_sync(
user_id: str,
request: UpdateUserRequest,
) -> tuple[dict[str, Any], dict[str, Any], bool, str | None]:
with get_db_context() as db:
existing_user = UserService.get_user(db, user_id)
if not existing_user:
raise NotFoundException("用户不存在", "user")
update_data = request.model_dump(exclude_unset=True)
old_role = existing_user.role
existing_wallet = WalletService.get_or_create_wallet(db, user=existing_user)
unlimited_before = WalletService.is_unlimited_wallet(existing_wallet)
requested_unlimited = update_data.pop("unlimited", None)
if "role" in update_data and update_data["role"]:
if not hasattr(update_data["role"], "value"):
update_data["role"] = UserRole[update_data["role"].upper()]
user = UserService.update_user(db, user_id, **update_data)
if not user:
raise NotFoundException("用户不存在", "user")
changed_fields = list(update_data.keys())
if requested_unlimited is not None:
wallet = WalletService.get_or_create_wallet(db, user=user)
if wallet is not None:
WalletService.set_wallet_limit_mode(
db,
wallet=wallet,
limit_mode="unlimited" if requested_unlimited else "finite",
)
changed_fields.append("unlimited")
role_changed = "role" in update_data and update_data["role"] != old_role
response = _serialize_user(db, user)
return (
response,
{
"action": "update_user",
"target_user_id": user.id,
"updated_fields": changed_fields,
"role_before": old_role.value if old_role else None,
"role_after": user.role.value,
"unlimited_before": unlimited_before,
"unlimited_after": (
requested_unlimited if requested_unlimited is not None else unlimited_before
),
"is_active": user.is_active,
},
role_changed,
user.email,
)
def _delete_user_sync(user_id: str) -> tuple[dict[str, Any], dict[str, Any], str | None]:
with get_db_context() as db:
user = UserService.get_user(db, user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
if user.role == UserRole.ADMIN:
admin_count = int(
db.query(func.count(User.id)).filter(User.role == UserRole.ADMIN).scalar() or 0
)
if admin_count <= 1:
raise InvalidRequestException("不能删除最后一个管理员账户")
try:
success = UserService.delete_user(db, user_id)
except ValueError as exc:
raise InvalidRequestException(str(exc)) from exc
if not success:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
return (
{"message": "用户删除成功"},
{
"action": "delete_user",
"target_user_id": user.id,
"target_email": user.email,
"target_role": user.role.value,
},
user.email,
)
def _list_user_sessions_sync(user_id: str) -> tuple[list[dict[str, Any]], dict[str, Any]]:
with get_db_context() as db:
user = UserService.get_user(db, user_id)
if not user:
raise NotFoundException("用户不存在", "user")
sessions = SessionService.list_user_sessions(db, user_id=user_id)
return (
[UserSessionResponse.from_db(s) for s in sessions],
{
"action": "list_user_sessions",
"target_user_id": user_id,
"session_count": len(sessions),
},
)
def _revoke_user_session_sync(
user_id: str,
session_id: str,
admin_user_id: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
with get_db_context() as db:
user = UserService.get_user(db, user_id)
if not user:
raise NotFoundException("用户不存在", "user")
session = SessionService.get_session_for_user(db, user_id=user_id, session_id=session_id)
if not session:
raise NotFoundException("会话不存在", "session")
SessionService.revoke_session(
db,
session=session,
reason="admin_session_revoked",
audit_user_id=admin_user_id,
)
return (
{"message": "用户设备已强制下线"},
{
"action": "revoke_user_session",
"target_user_id": user_id,
"session_id": session_id,
},
)
def _revoke_all_user_sessions_sync(
user_id: str, admin_user_id: str
) -> tuple[dict[str, Any], dict[str, Any]]:
with get_db_context() as db:
user = UserService.get_user(db, user_id)
if not user:
raise NotFoundException("用户不存在", "user")
revoked_count = SessionService.revoke_all_user_sessions(
db,
user_id=user_id,
reason="admin_revoke_all_sessions",
)
return (
{"message": "已强制下线该用户所有设备", "revoked_count": revoked_count},
{
"action": "revoke_all_user_sessions",
"target_user_id": user_id,
"admin_user_id": admin_user_id,
"revoked_count": revoked_count,
},
)
def _create_user_key_sync(
user_id: str,
key_data: CreateApiKeyRequest,
) -> tuple[dict[str, Any], dict[str, Any], str | None]:
with get_db_context() as db:
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise NotFoundException("用户不存在", "user")
api_key, plain_key = ApiKeyService.create_api_key(
db=db,
user_id=user_id,
name=key_data.name,
allowed_providers=key_data.allowed_providers,
allowed_models=key_data.allowed_models,
rate_limit=key_data.rate_limit,
expire_days=key_data.expire_days,
is_standalone=False,
)
return (
{
"id": api_key.id,
"key": plain_key,
"name": api_key.name,
"key_display": api_key.get_display_key(),
"rate_limit": api_key.rate_limit,
"expires_at": api_key.expires_at.isoformat() if api_key.expires_at else None,
"created_at": api_key.created_at.isoformat(),
"message": "API Key创建成功请妥善保存完整密钥",
},
{
"action": "create_user_api_key",
"target_user_id": user_id,
"key_id": api_key.id,
},
user.email,
)
def _delete_user_key_sync(user_id: str, key_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
with get_db_context() as db:
api_key = (
db.query(ApiKey)
.filter(
ApiKey.id == key_id,
ApiKey.user_id == user_id,
ApiKey.is_standalone == False,
)
.first()
)
if not api_key:
raise NotFoundException("API Key不存在或不属于该用户", "api_key")
pre_clean_api_key(db, api_key.id)
db.delete(api_key)
return {"message": "API Key已删除"}, {
"action": "delete_user_api_key",
"target_user_id": user_id,
"key_id": key_id,
}
def _update_user_key_sync(
user_id: str,
key_id: str,
request: UpdateMyApiKeyRequest,
) -> tuple[dict[str, Any], dict[str, Any]]:
with get_db_context() as db:
api_key = (
db.query(ApiKey)
.filter(
ApiKey.id == key_id,
ApiKey.user_id == user_id,
ApiKey.is_standalone == False,
)
.first()
)
if not api_key:
raise NotFoundException("API Key不存在或不属于该用户", "api_key")
update_data = request.model_dump(exclude_unset=True)
if "rate_limit" in update_data and update_data["rate_limit"] is None:
update_data["rate_limit"] = 0
updated_key = ApiKeyService.update_api_key(db, key_id, **update_data)
if not updated_key:
raise NotFoundException("API Key不存在或不属于该用户", "api_key")
return (
{
"id": updated_key.id,
"name": updated_key.name,
"key_display": updated_key.get_display_key(),
"is_active": updated_key.is_active,
"is_locked": updated_key.is_locked,
"total_requests": updated_key.total_requests,
"total_cost_usd": float(updated_key.total_cost_usd or 0),
"rate_limit": updated_key.rate_limit,
"expires_at": (
updated_key.expires_at.isoformat() if updated_key.expires_at else None
),
"last_used_at": (
updated_key.last_used_at.isoformat() if updated_key.last_used_at else None
),
"created_at": updated_key.created_at.isoformat(),
"message": "API Key更新成功",
},
{
"action": "update_user_api_key",
"target_user_id": user_id,
"key_id": key_id,
"updated_fields": list(update_data.keys()),
},
)
def _toggle_user_key_lock_sync(
user_id: str,
key_id: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
with get_db_context() as db:
api_key = (
db.query(ApiKey)
.filter(
ApiKey.id == key_id,
ApiKey.user_id == user_id,
ApiKey.is_standalone == False,
)
.first()
)
if not api_key:
raise NotFoundException("API Key不存在或不属于该用户", "api_key")
api_key.is_locked = not api_key.is_locked
db.commit()
db.refresh(api_key)
return (
{
"id": api_key.id,
"is_locked": api_key.is_locked,
"message": f"API密钥已{'锁定' if api_key.is_locked else '解锁'}",
},
{
"action": "toggle_user_api_key_lock",
"target_user_id": user_id,
"key_id": key_id,
"new_lock_status": "locked" if api_key.is_locked else "unlocked",
},
)
2025-12-10 20:52:44 +08:00
# 管理员端点
@router.post("")
async def create_user_endpoint(request: Request, db: Session = Depends(get_db)) -> Any:
"""
创建用户
创建新用户账号管理员专用
**请求体**:
- `email`: 邮箱地址
- `username`: 用户名
- `password`: 密码
- `role`: 角色user/admin
- `initial_gift_usd`: 初始赠款USD可选
- `unlimited`: 是否无限制
"""
2025-12-10 20:52:44 +08:00
adapter = AdminCreateUserAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("")
async def list_users(
request: Request,
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=1000, description="返回记录数"),
role: str | None = Query(None, description="按角色筛选user/admin"),
is_active: bool | None = Query(None, description="按状态筛选"),
2025-12-10 20:52:44 +08:00
db: Session = Depends(get_db),
) -> Any:
"""
获取用户列表
分页获取用户列表支持按角色和状态筛选
**返回字段**: id, email, username, role, unlimited, is_active, created_at
"""
2025-12-10 20:52:44 +08:00
adapter = AdminListUsersAdapter(skip=skip, limit=limit, role=role, is_active=is_active)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{user_id}")
async def get_user(user_id: str, request: Request, db: Session = Depends(get_db)) -> Any:
"""
获取用户详情
获取指定用户的详细信息
**路径参数**:
- `user_id`: 用户 ID (UUID)
"""
2025-12-10 20:52:44 +08:00
adapter = AdminGetUserAdapter(user_id=user_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{user_id}/sessions")
async def list_user_sessions(user_id: str, request: Request, db: Session = Depends(get_db)) -> Any:
"""获取用户登录设备列表。"""
adapter = AdminListUserSessionsAdapter(user_id=user_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.delete("/{user_id}/sessions/{session_id}")
async def revoke_user_session(
user_id: str,
session_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""强制下线用户的单个设备会话。"""
adapter = AdminRevokeUserSessionAdapter(user_id=user_id, session_id=session_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.delete("/{user_id}/sessions")
async def revoke_all_user_sessions(
user_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""强制下线用户的全部设备会话。"""
adapter = AdminRevokeAllUserSessionsAdapter(user_id=user_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
2025-12-10 20:52:44 +08:00
@router.put("/{user_id}")
async def update_user(
user_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
更新用户信息
更新指定用户的信息包括角色无限制开关权限等
**路径参数**:
- `user_id`: 用户 ID (UUID)
**请求体** (均为可选):
- `email`: 邮箱地址
- `username`: 用户名
- `role`: 角色
- `unlimited`: 是否无限制
- `is_active`: 是否启用
- `allowed_providers`: 允许的提供商列表
- `allowed_models`: 允许的模型列表
"""
2025-12-10 20:52:44 +08:00
adapter = AdminUpdateUserAdapter(user_id=user_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.delete("/{user_id}")
async def delete_user(user_id: str, request: Request, db: Session = Depends(get_db)) -> None:
"""
删除用户
永久删除指定用户不能删除最后一个管理员账户
**路径参数**:
- `user_id`: 用户 ID (UUID)
"""
2025-12-10 20:52:44 +08:00
adapter = AdminDeleteUserAdapter(user_id=user_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{user_id}/api-keys")
async def get_user_api_keys(
user_id: str,
request: Request,
is_active: bool | None = Query(None, description="按状态筛选"),
2025-12-10 20:52:44 +08:00
db: Session = Depends(get_db),
) -> Any:
"""
获取用户的 API 密钥列表
获取指定用户的所有 API 密钥不包括独立密钥
**路径参数**:
- `user_id`: 用户 ID (UUID)
"""
2025-12-10 20:52:44 +08:00
adapter = AdminGetUserKeysAdapter(user_id=user_id, is_active=is_active)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.post("/{user_id}/api-keys")
async def create_user_api_key(
user_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
为用户创建 API 密钥
为指定用户创建新的 API 密钥
**路径参数**:
- `user_id`: 用户 ID (UUID)
**请求体**:
- `name`: 密钥名称
- `allowed_providers`: 允许的提供商可选
- `allowed_models`: 允许的模型可选
- `rate_limit`: 速率限制可选
- `expire_days`: 过期天数可选
**返回**: 包含完整密钥值的响应仅此一次显示
"""
2025-12-10 20:52:44 +08:00
adapter = AdminCreateUserKeyAdapter(user_id=user_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.delete("/{user_id}/api-keys/{key_id}")
async def delete_user_api_key(
user_id: str,
key_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
删除用户的 API 密钥
删除指定用户的指定 API 密钥
**路径参数**:
- `user_id`: 用户 ID (UUID)
- `key_id`: 密钥 ID
"""
2025-12-10 20:52:44 +08:00
adapter = AdminDeleteUserKeyAdapter(user_id=user_id, key_id=key_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.put("/{user_id}/api-keys/{key_id}")
async def update_user_api_key(
user_id: str,
key_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
更新用户的 API 密钥
更新指定用户的普通 API 密钥基础配置
**路径参数**:
- `user_id`: 用户 ID (UUID)
- `key_id`: 密钥 ID
"""
adapter = AdminUpdateUserKeyAdapter(user_id=user_id, key_id=key_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.patch("/{user_id}/api-keys/{key_id}/lock")
async def toggle_user_api_key_lock(
user_id: str,
key_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
切换用户 API 密钥锁定状态
仅支持普通用户 Key非独立 Key
**路径参数**:
- `user_id`: 用户 ID (UUID)
- `key_id`: 密钥 ID
"""
adapter = AdminToggleUserKeyLockAdapter(user_id=user_id, key_id=key_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{user_id}/api-keys/{key_id}/full-key")
async def get_user_api_key_full_key(
user_id: str,
key_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
获取用户 API 密钥完整值
仅支持普通用户 Key非独立 Key
**路径参数**:
- `user_id`: 用户 ID (UUID)
- `key_id`: 密钥 ID
"""
adapter = AdminGetUserKeyFullKeyAdapter(user_id=user_id, key_id=key_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
2025-12-10 20:52:44 +08:00
# ============== 管理员适配器实现 ==============
class AdminCreateUserAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
payload = context.ensure_json_body()
try:
request = CreateUserRequest.model_validate(payload)
except ValidationError as e:
errors = e.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
try:
role = (
request.role if hasattr(request.role, "value") else UserRole[request.role.upper()]
)
except (KeyError, AttributeError):
raise InvalidRequestException("角色参数不合法")
try:
response, audit_meta = await run_in_threadpool(_create_user_sync, request, role)
2025-12-10 20:52:44 +08:00
except ValueError as exc:
raise InvalidRequestException(str(exc)) from exc
context.add_audit_metadata(**audit_meta)
return response
2025-12-10 20:52:44 +08:00
class AdminListUsersAdapter(AdminApiAdapter):
def __init__(self, skip: int, limit: int, role: str | None, is_active: bool | None):
2025-12-10 20:52:44 +08:00
self.skip = skip
self.limit = limit
self.role = role
self.is_active = is_active
@cache_result(
key_prefix="admin:users:list",
ttl=CacheTTL.USER,
user_specific=False,
vary_by=["skip", "limit", "role", "is_active"],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
db = context.db
try:
role_enum = UserRole[self.role.upper()] if self.role else None
except KeyError as exc:
raise InvalidRequestException("角色参数不合法") from exc
users = UserService.list_users(db, self.skip, self.limit, role_enum, self.is_active)
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
wallets_by_user_id = WalletService.get_wallets_by_user_ids(db, [user.id for user in users])
return [_serialize_user(db, user, wallets_by_user_id.get(user.id)) for user in users]
2025-12-10 20:52:44 +08:00
class AdminGetUserAdapter(AdminApiAdapter):
def __init__(self, user_id: str):
self.user_id = user_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
db = context.db
user = UserService.get_user(db, self.user_id)
if not user:
raise NotFoundException("用户不存在", "user")
context.add_audit_metadata(
action="get_user_detail",
target_user_id=user.id,
target_role=user.role.value,
include_history=bool(user.last_login_at),
)
return _serialize_user(db, user)
2025-12-10 20:52:44 +08:00
class AdminListUserSessionsAdapter(AdminApiAdapter):
def __init__(self, user_id: str):
self.user_id = user_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
response, audit_meta = await run_in_threadpool(_list_user_sessions_sync, self.user_id)
context.add_audit_metadata(**audit_meta)
return response
class AdminRevokeUserSessionAdapter(AdminApiAdapter):
def __init__(self, user_id: str, session_id: str):
self.user_id = user_id
self.session_id = session_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
admin_user_id = context.user.id if context.user else ""
response, audit_meta = await run_in_threadpool(
_revoke_user_session_sync, self.user_id, self.session_id, admin_user_id
)
context.add_audit_metadata(**audit_meta)
return response
class AdminRevokeAllUserSessionsAdapter(AdminApiAdapter):
def __init__(self, user_id: str):
self.user_id = user_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
admin_user_id = context.user.id if context.user else ""
response, audit_meta = await run_in_threadpool(
_revoke_all_user_sessions_sync, self.user_id, admin_user_id
)
context.add_audit_metadata(**audit_meta)
return response
2025-12-10 20:52:44 +08:00
class AdminUpdateUserAdapter(AdminApiAdapter):
def __init__(self, user_id: str):
self.user_id = user_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
payload = context.ensure_json_body()
try:
request = UpdateUserRequest.model_validate(payload)
except ValidationError as e:
errors = e.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
response, audit_meta, role_changed, user_email = await run_in_threadpool(
_update_user_sync,
self.user_id,
request,
)
if user_email:
await UserCacheService.invalidate_user_cache(self.user_id, user_email)
if role_changed:
from src.services.usage.service import UsageService
await UsageService.clear_user_heatmap_cache(self.user_id)
context.add_audit_metadata(**audit_meta)
return response
2025-12-10 20:52:44 +08:00
class AdminDeleteUserAdapter(AdminApiAdapter):
def __init__(self, user_id: str):
self.user_id = user_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
response, audit_meta, user_email = await run_in_threadpool(_delete_user_sync, self.user_id)
if user_email:
await UserCacheService.invalidate_user_cache(self.user_id, user_email)
context.add_audit_metadata(**audit_meta)
return response
2025-12-10 20:52:44 +08:00
class AdminGetUserKeysAdapter(AdminApiAdapter):
"""获取用户的API Keys"""
def __init__(self, user_id: str, is_active: bool | None):
2025-12-10 20:52:44 +08:00
self.user_id = user_id
self.is_active = is_active
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
db = context.db
# 验证用户存在
user = db.query(User).filter(User.id == self.user_id).first()
if not user:
raise NotFoundException("用户不存在", "user")
# 获取用户的Keys不包括独立Keys
api_keys = ApiKeyService.list_user_api_keys(
db=db, user_id=self.user_id, is_active=self.is_active
)
context.add_audit_metadata(
action="list_user_api_keys",
target_user_id=self.user_id,
total=len(api_keys),
)
return {
"api_keys": [
{
"id": key.id,
"name": key.name,
"key_display": key.get_display_key(),
"is_active": key.is_active,
"is_locked": key.is_locked,
2025-12-10 20:52:44 +08:00
"total_requests": key.total_requests,
"total_cost_usd": float(key.total_cost_usd or 0),
"rate_limit": key.rate_limit,
"expires_at": key.expires_at.isoformat() if key.expires_at else None,
"last_used_at": key.last_used_at.isoformat() if key.last_used_at else None,
"created_at": key.created_at.isoformat(),
}
for key in api_keys
],
"total": len(api_keys),
"user_email": user.email,
"username": user.username,
}
class AdminCreateUserKeyAdapter(AdminApiAdapter):
"""为用户创建API Key"""
def __init__(self, user_id: str):
self.user_id = user_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
payload = context.ensure_json_body()
try:
key_data = CreateApiKeyRequest.model_validate(payload)
except ValidationError as e:
errors = e.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
response, audit_meta, user_email = await run_in_threadpool(
_create_user_key_sync,
self.user_id,
key_data,
2025-12-10 20:52:44 +08:00
)
if user_email:
logger.info(
"管理员为用户创建API Key: 用户 {}, Key ID {}", user_email, audit_meta["key_id"]
)
context.add_audit_metadata(**audit_meta)
return response
2025-12-10 20:52:44 +08:00
class AdminDeleteUserKeyAdapter(AdminApiAdapter):
"""删除用户的API Key"""
def __init__(self, user_id: str, key_id: str):
self.user_id = user_id
self.key_id = key_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
response, audit_meta = await run_in_threadpool(
_delete_user_key_sync,
self.user_id,
self.key_id,
2025-12-10 20:52:44 +08:00
)
context.add_audit_metadata(**audit_meta)
return response
class AdminUpdateUserKeyAdapter(AdminApiAdapter):
"""更新用户的普通 API Key"""
def __init__(self, user_id: str, key_id: str):
self.user_id = user_id
self.key_id = key_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
payload = context.ensure_json_body()
try:
request = UpdateMyApiKeyRequest.model_validate(payload)
except ValidationError as e:
errors = e.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
response, audit_meta = await run_in_threadpool(
_update_user_key_sync,
self.user_id,
self.key_id,
request,
)
context.add_audit_metadata(**audit_meta)
return response
class AdminToggleUserKeyLockAdapter(AdminApiAdapter):
"""切换用户普通 API Key 的锁定状态"""
def __init__(self, user_id: str, key_id: str):
self.user_id = user_id
self.key_id = key_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
response, audit_meta = await run_in_threadpool(
_toggle_user_key_lock_sync,
self.user_id,
self.key_id,
)
context.add_audit_metadata(**audit_meta)
return response
class AdminGetUserKeyFullKeyAdapter(AdminApiAdapter):
"""获取用户普通 API Key 的完整密钥"""
def __init__(self, user_id: str, key_id: str):
self.user_id = user_id
self.key_id = key_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
from src.core.crypto import crypto_service
db = context.db
api_key = (
db.query(ApiKey)
.filter(
ApiKey.id == self.key_id,
ApiKey.user_id == self.user_id,
ApiKey.is_standalone == False, # 仅普通用户Key
)
.first()
)
if not api_key:
raise NotFoundException("API Key不存在或不属于该用户", "api_key")
if not api_key.key_encrypted:
raise InvalidRequestException("该密钥没有存储完整密钥信息")
try:
full_key = crypto_service.decrypt(api_key.key_encrypted)
except Exception as exc:
logger.error(
f"解密用户API密钥失败: 用户ID {self.user_id}, Key ID {self.key_id}, 错误: {exc}"
)
raise HTTPException(status_code=500, detail="解密密钥失败")
context.add_audit_metadata(
action="view_user_api_key_full_key",
target_user_id=self.user_id,
key_id=self.key_id,
)
return {"key": full_key}