Files
trade-assistant/backend/app/api/v1/admin.py
T
TradeMate Dev c397740748 feat: WeChat Pay integration, translation quota management, login UX fixes
- WeChat Pay APIv3 integration (JSAPI + Native) with cert-based auth
- TranslationQuota model + admin management UI (配额 tab)
- Alibaba MT provider now checks quota before translation
- Fix: admin tabs scrollable on mobile, remove header-card
- Fix: profile/login navigation - logout stays on profile, login returns to profile
- Fix: login form now visible by default (no extra click to show)
- Fix: home page translate link uses navigateTo (was switchTab to non-tabBar page)
- Add .coverage and apiclient_key.pem to gitignore
2026-05-20 18:30:12 +08:00

215 lines
6.3 KiB
Python

import uuid
from typing import Optional
from datetime import date, datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.services.admin import AdminService
from app.services.translation_quota import TranslationQuotaService
from app.api.v1.deps import get_current_user
router = APIRouter()
async def require_admin(current_user: dict = Depends(get_current_user)) -> dict:
if current_user.get("role") != "admin":
raise HTTPException(status_code=403, detail="Admin access required")
return current_user
@router.get("/dashboard")
async def get_dashboard(
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = AdminService(db)
return await service.get_dashboard()
@router.get("/users")
async def list_users(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
role: Optional[str] = Query(None),
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = AdminService(db)
return await service.list_users(page, size, role)
def _validate_uuid(user_id: str):
try:
uuid.UUID(user_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid user ID format")
@router.patch("/users/{target_user_id}/tier")
async def update_user_tier(
target_user_id: str,
data: dict,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
_validate_uuid(target_user_id)
service = AdminService(db)
tier = data.get("tier")
if tier not in ("free", "pro", "enterprise"):
raise HTTPException(status_code=400, detail="Invalid tier")
success = await service.update_user_tier(target_user_id, tier)
if not success:
raise HTTPException(status_code=404, detail="User not found")
return {"message": f"User tier updated to {tier}"}
@router.post("/users/{target_user_id}/toggle-active")
async def toggle_user_active(
target_user_id: str,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
_validate_uuid(target_user_id)
service = AdminService(db)
success = await service.toggle_user_active(target_user_id)
if not success:
raise HTTPException(status_code=404, detail="User not found")
return {"message": "User active status toggled"}
@router.patch("/users/{target_user_id}/role")
async def update_user_role(
target_user_id: str,
data: dict,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
_validate_uuid(target_user_id)
service = AdminService(db)
role = data.get("role")
if role not in ("user", "admin"):
raise HTTPException(status_code=400, detail="Invalid role. Must be 'user' or 'admin'")
result = await service.update_user_role(target_user_id, role)
if not result:
raise HTTPException(status_code=404, detail="User not found")
return result
@router.get("/users/search")
async def search_users(
q: str = Query(..., min_length=1),
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = AdminService(db)
return await service.search_users(q)
@router.get("/users/{target_user_id}")
async def get_user_detail(
target_user_id: str,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
_validate_uuid(target_user_id)
service = AdminService(db)
result = await service.get_user_detail(target_user_id)
if not result:
raise HTTPException(status_code=404, detail="User not found")
return result
@router.get("/usage-stats")
async def get_usage_stats(
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = AdminService(db)
return await service.get_usage_stats()
@router.get("/logs")
async def get_logs(
page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=200),
action: Optional[str] = Query(None),
user_id: Optional[str] = Query(None),
date_from: Optional[date] = Query(None),
date_to: Optional[date] = Query(None),
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = AdminService(db)
dt_from = datetime.combine(date_from, datetime.min.time()) if date_from else None
dt_to = datetime.combine(date_to, datetime.max.time()) if date_to else None
return await service.get_logs(page, size, action, user_id, dt_from, dt_to)
@router.get("/config")
async def list_config(
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = AdminService(db)
return await service.list_config()
@router.put("/config/{key}")
async def update_config(
key: str,
data: dict,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = AdminService(db)
item = await service.update_config(key, data.get("value"))
if not item:
raise HTTPException(status_code=404, detail="Config not found")
return item
@router.get("/health")
async def system_health(
db: AsyncSession = Depends(get_db),
):
service = AdminService(db)
return await service.get_system_health()
@router.get("/translation-quotas")
async def list_translation_quotas(
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = TranslationQuotaService(db)
return await service.get_all_quotas()
@router.put("/translation-quotas/{version}")
async def update_translation_quota(
version: str,
data: dict,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
allowed = {"monthly_limit", "enabled", "description"}
filtered = {k: v for k, v in data.items() if k in allowed}
service = TranslationQuotaService(db)
result = await service.update_quota(version, filtered)
if not result:
raise HTTPException(status_code=404, detail="Quota not found")
return result
@router.post("/translation-quotas/{version}/reset")
async def reset_translation_quota(
version: str,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = TranslationQuotaService(db)
result = await service.reset_usage(version)
if not result:
raise HTTPException(status_code=404, detail="Quota not found")
return result