用户生成资源容量管控

This commit is contained in:
2026-06-29 13:50:39 +08:00
parent b21f35582e
commit 25fa41f894
21 changed files with 1314 additions and 80 deletions
+2
View File
@@ -1,6 +1,8 @@
from fastapi import APIRouter
from app.api.admin.video_prompt_schema_config import router as video_prompt_schema_config_router
from app.api.admin.resource_capacity import router as resource_capacity_router
router = APIRouter()
router.include_router(video_prompt_schema_config_router)
router.include_router(resource_capacity_router)
@@ -0,0 +1,175 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, Path
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_admin_user, get_db
from app.enums.resource_capacity import ResourceCapacityOperationEnum
from app.models.user import User
from app.schemas.resource_capacity import (
AdminUserResourceCapacityOut,
ResourceCapacityConfigOut,
ResourceCapacityConfigUpdate,
)
from app.services.operation_log import log_operation
from app.services.resource_capacity_service import (
build_global_resource_capacity_operation_detail,
build_user_resource_capacity_operation_detail,
delete_user_resource_capacity_config,
get_admin_user_resource_capacity,
get_global_resource_capacity_config,
save_global_resource_capacity_config,
save_user_resource_capacity_config,
)
router = APIRouter(prefix="/admin", tags=["admin-resource-capacity"])
@router.get(
"/resource-capacity/global",
response_model=ResourceCapacityConfigOut,
summary="获取全局资源空间容量配置",
description=(
"获取管理后台全局生成资源空间容量管控配置。"
"配置最终存储在 system_configs 表,key=resource_capacity_limit_config。"
"enabled=false 表示全局容量管控关闭;enabled=true 表示按 limit_value + limit_unit 换算出的 limit_bytes 进行限制。"
"单位枚举:MB=1048576字节,GB=1073741824字节,TB=1099511627776字节。"
),
)
async def get_global_resource_capacity(
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
_ = admin
return await get_global_resource_capacity_config(db)
@router.put(
"/resource-capacity/global",
response_model=ResourceCapacityConfigOut,
summary="保存全局资源空间容量配置",
description=(
"保存管理后台全局生成资源空间容量管控配置。"
"enabled=true 时,limit_value 和 limit_unit 必填。"
"limit_value 最小为1,不能为负数,最多支持3位小数。"
"limit_unit 枚举明细:MB=1048576字节,GB=1073741824字节,TB=1099511627776字节。"
"limit_bytes 不允许前端传入,由后端统一换算后保存。"
"本接口会写入后台操作日志,action=更新全局资源容量配置,detail记录修改前后配置快照。"
),
)
async def update_global_resource_capacity(
req: ResourceCapacityConfigUpdate,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
before = await get_global_resource_capacity_config(db)
result = await save_global_resource_capacity_config(db, req)
await log_operation(
db,
admin.id,
admin.username,
ResourceCapacityOperationEnum.UPDATE_GLOBAL_CONFIG.value,
"PUT",
"/admin/resource-capacity/global",
detail=build_global_resource_capacity_operation_detail(before, result),
)
await db.commit()
return result
@router.get(
"/users/{user_id}/resource-capacity",
response_model=AdminUserResourceCapacityOut,
summary="获取指定用户资源空间容量配置",
description=(
"获取指定用户的个人容量配置、全局容量配置以及最终生效容量数据。"
"优先级:用户个人配置存在时优先使用;用户个人配置不存在时使用全局配置。"
"注意:用户个人配置存在且 enabled=false,表示该用户个人明确关闭容量限制,不再回落到全局配置。"
),
)
async def get_user_resource_capacity(
user_id: str = Path(..., description="用户ID,用于查询该用户个人容量配置和最终生效容量数据"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
_ = admin
return await get_admin_user_resource_capacity(db, user_id)
@router.put(
"/users/{user_id}/resource-capacity",
response_model=AdminUserResourceCapacityOut,
summary="保存指定用户个人资源空间容量配置",
description=(
"新增或更新指定用户个人资源空间容量配置。"
"保存后该用户配置优先级高于全局配置。"
"enabled=true 表示启用该用户个人容量限制;enabled=false 表示该用户个人明确关闭限制,不再回落到全局。"
"limit_value 最小为1,不能为负数,最多3位小数。"
"limit_unit 枚举明细:MB=1048576字节,GB=1073741824字节,TB=1099511627776字节。"
"本接口会写入后台操作日志:首次创建 action=新增用户资源容量配置;已有配置更新 action=更新用户资源容量配置;detail记录修改前后配置快照。"
),
)
async def update_user_resource_capacity(
req: ResourceCapacityConfigUpdate,
user_id: str = Path(..., description="用户ID,用于新增或更新该用户个人容量配置"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
before = await get_admin_user_resource_capacity(db, user_id)
result = await save_user_resource_capacity_config(db, user_id, req)
is_create = not before.has_user_config
await log_operation(
db,
admin.id,
admin.username,
(
ResourceCapacityOperationEnum.CREATE_USER_CONFIG.value
if is_create
else ResourceCapacityOperationEnum.UPDATE_USER_CONFIG.value
),
"PUT",
f"/admin/users/{user_id}/resource-capacity",
detail=build_user_resource_capacity_operation_detail(
target_user_id=user_id,
operation="create" if is_create else "update",
before=before,
after=result,
),
)
await db.commit()
return result
@router.delete(
"/users/{user_id}/resource-capacity",
response_model=AdminUserResourceCapacityOut,
summary="删除指定用户个人资源空间容量配置",
description=(
"删除指定用户个人容量配置。删除后该用户不再有个人覆盖配置,后续容量限制重新回落到全局配置。"
"本接口会写入后台操作日志,action=删除用户资源容量配置,detail记录删除前个人配置、删除后最终生效配置。"
),
)
async def remove_user_resource_capacity(
user_id: str = Path(..., description="用户ID,用于删除该用户个人容量配置并恢复走全局配置"),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
before = await get_admin_user_resource_capacity(db, user_id)
result = await delete_user_resource_capacity_config(db, user_id)
await log_operation(
db,
admin.id,
admin.username,
ResourceCapacityOperationEnum.DELETE_USER_CONFIG.value,
"DELETE",
f"/admin/users/{user_id}/resource-capacity",
detail=build_user_resource_capacity_operation_detail(
target_user_id=user_id,
operation="delete",
before=before,
after=result,
remark="删除用户个人容量配置,用户恢复使用全局资源容量配置。",
),
)
await db.commit()
return result
+15 -2
View File
@@ -49,6 +49,7 @@ from app.services.auth import hash_password, verify_password
from app.services.operation_log import log_operation
from app.services.resource_signed_url_service import build_resource_signed_url
from app.services.payment import sync_pending_orders, process_refund
from app.services.resource_capacity_service import batch_get_user_resource_capacity_usage, get_user_resource_capacity_usage
from app.services.generation_billing_service import (
OWNER_GENERATION_RECORD,
@@ -107,7 +108,16 @@ async def list_users(
total = (await db.execute(count_query)).scalar() or 0
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
items = result.scalars().all()
return {"items": [AdminUserOut.model_validate(u) for u in items], "total": total}
capacity_map = await batch_get_user_resource_capacity_usage(db, [u.id for u in items])
return {
"items": [
AdminUserOut.model_validate(u)
.model_copy(update={"resource_capacity": capacity_map.get(u.id)})
.model_dump(mode="json")
for u in items
],
"total": total,
}
@router.post("/users", response_model=AdminUserOut)
@@ -183,7 +193,10 @@ async def get_user(
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
user.credits = round(user.credits, 2)
return user
resource_capacity = await get_user_resource_capacity_usage(db, user.id)
return AdminUserOut.model_validate(user).model_copy(
update={"resource_capacity": resource_capacity}
)
@router.post("/users/{user_id}/credits")
+9 -2
View File
@@ -30,6 +30,7 @@ from app.services.auth import (
verify_password,
)
from app.services.sms import verify_sms_code
from app.services.resource_capacity_service import get_user_resource_capacity_usage
from app.utils.id_gen import generate_id
router = APIRouter(prefix="/auth", tags=["auth"])
@@ -248,9 +249,15 @@ async def logout(current_user: User = Depends(get_current_user_allow_password_pe
@router.get("/me", response_model=UserOut)
async def get_me(current_user: User = Depends(get_current_user_allow_password_pending)):
async def get_me(
current_user: User = Depends(get_current_user_allow_password_pending),
db: AsyncSession = Depends(get_db),
):
current_user.credits = round(current_user.credits, 2)
return current_user
resource_capacity = await get_user_resource_capacity_usage(db, current_user.id)
return UserOut.model_validate(current_user).model_copy(
update={"resource_capacity": resource_capacity}
)
@router.post(
+5
View File
@@ -34,6 +34,7 @@ from app.services.resource_accounting_service import (
safe_file_size,
)
from app.services.resource_signed_url_service import build_resource_signed_url
from app.services.resource_capacity_service import assert_user_resource_capacity_available
from app.services.generation_billing_service import (
CHARGE_TEXT_PROMPT,
OWNER_GENERATION_RECORD,
@@ -398,6 +399,8 @@ async def generate(
if record.status not in ("prompt_optimized", "failed"):
raise InvalidStatusError("当前状态不允许生成")
await assert_user_resource_capacity_available(db, current_user.id)
attempt_no = await get_next_credit_attempt_no(
db,
owner_type=OWNER_GENERATION_RECORD,
@@ -522,6 +525,8 @@ async def retry_generation(
if record.status != "failed":
raise InvalidStatusError("只有失败的记录可以重试")
await assert_user_resource_capacity_available(db, current_user.id)
attempt_no = await get_next_credit_attempt_no(
db,
owner_type=OWNER_GENERATION_RECORD,
@@ -33,6 +33,7 @@ from app.services.generation_billing_service import (
)
from app.services.generation_log_service import log_task_event
from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once
from app.services.resource_capacity_service import assert_user_resource_capacity_available
from app.tasks.celery_app import celery_app
router = APIRouter(
@@ -566,6 +567,8 @@ async def retry_task(
if task.status != "failed":
raise HTTPException(status_code=400, detail="只有失败任务可以重试")
await assert_user_resource_capacity_available(db, current_user.id)
attempt_no = await get_next_credit_attempt_no(
db,
owner_type=OWNER_CHAT_GENERATION_TASK,