1、增加消息推送的前台横幅显示
2、增加apikey的整体配额增加和修改记录显示
This commit is contained in:
@@ -17,6 +17,7 @@ from app.schemas.admin_api.api_key import (
|
||||
ApiKeyCreateResponse,
|
||||
ApiKeyListItem,
|
||||
ApiKeyListOut,
|
||||
ApiKeyQuotaAdjustRequest,
|
||||
ApiKeyRevealResponse,
|
||||
ApiKeyResponse,
|
||||
ApiKeyUpdateRequest,
|
||||
@@ -388,3 +389,47 @@ async def list_all_usage(
|
||||
"total": total,
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{key_id}/quota-adjust", response_model=ApiKeyListItem, summary="调整 API Key 配额")
|
||||
async def quota_adjust(
|
||||
req: ApiKeyQuotaAdjustRequest,
|
||||
key_id: str = Path(..., description="API Key ID"),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiKeyListItem:
|
||||
"""调整 API Key 配额(增加总额/重置已用/设置限额/修改周期)。"""
|
||||
key = await key_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(status_code=404, detail="API Key 不存在")
|
||||
|
||||
key, changes = await key_service.adjust_quota(
|
||||
db,
|
||||
key,
|
||||
action=req.action,
|
||||
quota_limit_delta=req.quota_limit_delta,
|
||||
quota_limit=req.quota_limit,
|
||||
quota_cycle=req.quota_cycle,
|
||||
)
|
||||
|
||||
# 审计日志
|
||||
try:
|
||||
from app.services.operation_log import log_operation
|
||||
await log_operation(
|
||||
db=db,
|
||||
user_id=str(admin.id),
|
||||
username=str(admin.username),
|
||||
action=f"quota_adjust:{req.action}",
|
||||
method="POST",
|
||||
path=f"/admin/api-keys/{key_id}/quota-adjust",
|
||||
detail=json.dumps(
|
||||
{**changes, "reason": req.reason},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
),
|
||||
)
|
||||
except Exception as log_exc:
|
||||
logger.warning("配额调整审计日志记录失败: %s", log_exc)
|
||||
|
||||
await db.commit()
|
||||
return _key_to_list_item(key)
|
||||
|
||||
@@ -1696,17 +1696,62 @@ async def update_system_config(
|
||||
return config
|
||||
|
||||
|
||||
@router.post("/system-configs/banner/reset", summary="重置活动横幅展示")
|
||||
async def reset_banner(
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""递增 site_banner_version,使所有用户再次看到横幅。"""
|
||||
from app.utils.id_gen import generate_id
|
||||
result = await db.execute(select(SystemConfig).where(SystemConfig.key == "site_banner_version").limit(1))
|
||||
config = result.scalar_one_or_none()
|
||||
new_version = 1
|
||||
if config:
|
||||
try:
|
||||
new_version = int(config.value or 0) + 1
|
||||
except ValueError:
|
||||
new_version = 1
|
||||
config.value = str(new_version)
|
||||
else:
|
||||
config = SystemConfig(
|
||||
id=generate_id(),
|
||||
key="site_banner_version",
|
||||
value=str(new_version),
|
||||
description="活动横幅版本号,递增后所有用户重新看到横幅",
|
||||
)
|
||||
db.add(config)
|
||||
await db.flush()
|
||||
await log_operation(
|
||||
db,
|
||||
admin.id,
|
||||
admin.username,
|
||||
f"重置活动横幅 (版本 → {new_version})",
|
||||
"POST",
|
||||
"/admin/system-configs/banner/reset",
|
||||
detail=json.dumps({"new_version": new_version}),
|
||||
)
|
||||
await db.commit()
|
||||
await invalidate_system_config_cache(["site_banner_version"])
|
||||
return {"site_banner_version": new_version}
|
||||
|
||||
|
||||
# ── Operation Logs ──────────────────────────────────────
|
||||
|
||||
@router.get("/operation-logs")
|
||||
async def list_operation_logs(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=500),
|
||||
action: str | None = Query(None, description="按 action 过滤(前缀匹配)"),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query = select(OperationLog).order_by(OperationLog.created_at.desc())
|
||||
count_query = select(func.count(OperationLog.id))
|
||||
|
||||
if action:
|
||||
query = query.where(OperationLog.action.like(f"{action}%"))
|
||||
count_query = count_query.where(OperationLog.action.like(f"{action}%"))
|
||||
|
||||
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()
|
||||
|
||||
@@ -342,7 +342,7 @@ async def get_site_info(db: AsyncSession = Depends(get_db)):
|
||||
"""Public endpoint returning site name, logo, agreement and copyright info."""
|
||||
result = await db.execute(
|
||||
select(SystemConfig).where(SystemConfig.key.in_([
|
||||
"site_name", "site_logo", "user_agreement_privacy_url", "site_copyright", "operation_manual", "login_bg_video", "optimize_hold_credits"
|
||||
"site_name", "site_logo", "user_agreement_privacy_url", "site_copyright", "operation_manual", "login_bg_video", "optimize_hold_credits", "site_banner", "site_banner_version"
|
||||
]))
|
||||
)
|
||||
configs = result.scalars().all()
|
||||
@@ -367,6 +367,8 @@ async def get_site_info(db: AsyncSession = Depends(get_db)):
|
||||
"operation_manual": info.get("operation_manual", ""),
|
||||
"login_bg_video": to_full_url(info.get("login_bg_video")) if info.get("login_bg_video") else "",
|
||||
"optimize_hold_credits": int(info.get("optimize_hold_credits") or 5),
|
||||
"site_banner": info.get("site_banner", ""),
|
||||
"site_banner_version": int(info.get("site_banner_version") or 0),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -133,6 +133,22 @@ class ApiKeyListItem(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ApiKeyQuotaAdjustRequest(BaseModel):
|
||||
"""配额调整请求。支持 camelCase 和 snake_case 两种字段名。"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
action: str = Field(
|
||||
...,
|
||||
pattern=r"^(adjust|reset_usage|set_limit|change_cycle)$",
|
||||
description="adjust=增加总额 | reset_usage=重置已用 | set_limit=设置限额 | change_cycle=修改周期",
|
||||
)
|
||||
quota_limit_delta: float | None = Field(None, ge=0, description="增加总额时的增量", alias="quotaLimitDelta")
|
||||
quota_limit: float | None = Field(None, description="设置新限额时的值(NULL=无限)", alias="quotaLimit")
|
||||
quota_cycle: str | None = Field(None, description="修改周期时的值", alias="quotaCycle")
|
||||
reason: str | None = Field(None, max_length=500, description="调整原因/备注")
|
||||
|
||||
|
||||
class ApiKeyListOut(BaseModel):
|
||||
"""API Key 列表响应。"""
|
||||
|
||||
|
||||
@@ -114,6 +114,50 @@ async def update_api_key(db: AsyncSession, key: ApiKey, **kwargs) -> ApiKey:
|
||||
return key
|
||||
|
||||
|
||||
async def adjust_quota(
|
||||
db: AsyncSession,
|
||||
key: ApiKey,
|
||||
action: str,
|
||||
quota_limit_delta: float | None = None,
|
||||
quota_limit: float | None = None,
|
||||
quota_cycle: str | None = None,
|
||||
) -> tuple[ApiKey, dict]:
|
||||
"""调整 API Key 配额。
|
||||
|
||||
返回 (更新后的 key, 变更详情 dict)。
|
||||
|
||||
action:
|
||||
- adjust: 增加总额,quota_limit_delta 累加到当前 quota_limit
|
||||
- reset_usage: 重置 quota_used 为 0
|
||||
- set_limit: 直接设置 quota_limit
|
||||
- change_cycle: 修改 quota_cycle
|
||||
"""
|
||||
old_limit = key.quota_limit
|
||||
old_used = key.quota_used
|
||||
old_cycle = key.quota_cycle
|
||||
|
||||
if action == "adjust":
|
||||
delta = quota_limit_delta or 0
|
||||
key.quota_limit = round((key.quota_limit or 0) + delta, 2)
|
||||
elif action == "reset_usage":
|
||||
key.quota_used = 0.0
|
||||
elif action == "set_limit":
|
||||
key.quota_limit = quota_limit # 允许设为 None(无限)
|
||||
elif action == "change_cycle":
|
||||
key.quota_cycle = quota_cycle # 允许设为 None(无限)
|
||||
else:
|
||||
raise ValueError(f"未知的调整操作: {action}")
|
||||
|
||||
await db.flush()
|
||||
|
||||
changes = {
|
||||
"old_limit": old_limit, "new_limit": key.quota_limit,
|
||||
"old_used": old_used, "new_used": key.quota_used,
|
||||
"old_cycle": old_cycle, "new_cycle": key.quota_cycle,
|
||||
}
|
||||
return key, changes
|
||||
|
||||
|
||||
async def delete_api_key(db: AsyncSession, key: ApiKey) -> None:
|
||||
"""软删除 API Key。"""
|
||||
key.deleted_at = datetime.now(timezone.utc)
|
||||
|
||||
Reference in New Issue
Block a user