Preserve local user table for superDream-specific features while syncing user lifecycle, API key CRUD and usage queries through sub2api. Admin token handles reads and user lifecycle; per-user tokens (Fernet-encrypted in DB) handle key writes that admin endpoints do not expose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
"""Usage service: fully proxied to sub2api (user JWT for user-scoped endpoints)."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.exceptions import BadRequestError
|
|
from app.integrations.sub2api import user as sub2api_user
|
|
from app.integrations.sub2api.client import (
|
|
Sub2APIError,
|
|
Sub2APIReauthRequired,
|
|
Sub2APITransportError,
|
|
)
|
|
from app.models import User
|
|
from app.services.sub2api_session import ensure_access_token
|
|
|
|
|
|
def _require_binding(user: User) -> int:
|
|
if not user.sub2api_user_id:
|
|
raise BadRequestError("账号未完成 sub2api 绑定,请重新登录")
|
|
return user.sub2api_user_id
|
|
|
|
|
|
def _translate(exc: Exception) -> HTTPException:
|
|
if isinstance(exc, Sub2APIReauthRequired):
|
|
return HTTPException(status_code=401, detail="sub2api_reauth_required")
|
|
if isinstance(exc, Sub2APIError):
|
|
status = exc.http_status or 502
|
|
if status < 400 or status >= 600:
|
|
status = 502
|
|
return HTTPException(status_code=status, detail=exc.message or exc.reason or "upstream_error")
|
|
return HTTPException(status_code=504, detail="upstream_timeout")
|
|
|
|
|
|
class UsageService:
|
|
@staticmethod
|
|
async def list_logs(db: AsyncSession, user: User, **params: Any) -> dict[str, Any]:
|
|
_require_binding(user)
|
|
try:
|
|
token = await ensure_access_token(db, user)
|
|
return await sub2api_user.list_usage(token, **params)
|
|
except (Sub2APIError, Sub2APITransportError) as exc:
|
|
raise _translate(exc) from exc
|
|
|
|
@staticmethod
|
|
async def stats(db: AsyncSession, user: User, **params: Any) -> dict[str, Any]:
|
|
_require_binding(user)
|
|
try:
|
|
token = await ensure_access_token(db, user)
|
|
return await sub2api_user.usage_stats(token, **params)
|
|
except (Sub2APIError, Sub2APITransportError) as exc:
|
|
raise _translate(exc) from exc
|
|
|
|
@staticmethod
|
|
async def dashboard_stats(db: AsyncSession, user: User) -> dict[str, Any]:
|
|
_require_binding(user)
|
|
try:
|
|
token = await ensure_access_token(db, user)
|
|
return await sub2api_user.dashboard_stats(token)
|
|
except (Sub2APIError, Sub2APITransportError) as exc:
|
|
raise _translate(exc) from exc
|
|
|
|
@staticmethod
|
|
async def dashboard_trend(db: AsyncSession, user: User, **params: Any) -> dict[str, Any]:
|
|
_require_binding(user)
|
|
try:
|
|
token = await ensure_access_token(db, user)
|
|
return await sub2api_user.dashboard_trend(token, **params)
|
|
except (Sub2APIError, Sub2APITransportError) as exc:
|
|
raise _translate(exc) from exc
|
|
|
|
@staticmethod
|
|
async def dashboard_models(db: AsyncSession, user: User, **params: Any) -> dict[str, Any]:
|
|
_require_binding(user)
|
|
try:
|
|
token = await ensure_access_token(db, user)
|
|
return await sub2api_user.dashboard_models(token, **params)
|
|
except (Sub2APIError, Sub2APITransportError) as exc:
|
|
raise _translate(exc) from exc
|
|
|
|
@staticmethod
|
|
async def dashboard_api_keys_usage(
|
|
db: AsyncSession, user: User, api_key_ids: list[int]
|
|
) -> dict[str, Any]:
|
|
_require_binding(user)
|
|
try:
|
|
token = await ensure_access_token(db, user)
|
|
return await sub2api_user.dashboard_api_keys_usage(token, api_key_ids)
|
|
except (Sub2APIError, Sub2APITransportError) as exc:
|
|
raise _translate(exc) from exc |