mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(wallet): 钱包系统替代配额系统,新增支付与退款机制
- 新增钱包余额管理、充值、扣费、退款完整流程 - 新增支付网关抽象层(支持手动/支付宝/微信) - 用量计费从配额系统迁移到钱包余额扣费 - 新增管理员钱包管理与支付订单管理页面 - 新增用户钱包中心页面 - 移除独立 Key 锁定机制,统一由钱包余额控制 - 新增相关 API 路由、序列化器与数据库迁移 - 新增钱包、支付、退款相关测试
This commit is contained in:
@@ -34,7 +34,7 @@ class ApiRequestContext:
|
||||
query_params: dict[str, str]
|
||||
raw_body: bytes | None = None
|
||||
json_body: dict[str, Any] | None = None
|
||||
quota_remaining: float | None = None
|
||||
balance_remaining: float | None = None
|
||||
mode: str = "standard" # standard / proxy
|
||||
api_format_hint: str | None = None
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from src.config.settings import config
|
||||
from src.core.enums import UserRole
|
||||
from src.core.exceptions import QuotaExceededException
|
||||
from src.core.exceptions import BalanceInsufficientException
|
||||
from src.core.logger import logger
|
||||
from src.models.database import ApiKey, AuditEventType, User
|
||||
from src.services.auth.service import AuthService
|
||||
from src.services.system.audit import AuditService
|
||||
from src.services.usage.service import UsageService
|
||||
from src.services.wallet import WalletService
|
||||
from src.utils.perf import PerfRecorder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -35,7 +36,7 @@ QUIET_POLLING_PATHS: set[str] = {
|
||||
|
||||
|
||||
class ApiRequestPipeline:
|
||||
"""负责统一执行认证、配额校验、上下文构建等通用逻辑的管道。"""
|
||||
"""负责统一执行认证、余额校验、上下文构建等通用逻辑的管道。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -164,7 +165,8 @@ class ApiRequestPipeline:
|
||||
# 存储 quiet 标志到 context,用于审计日志判断
|
||||
context.quiet_logging = is_quiet
|
||||
if mode != ApiMode.ADMIN and user:
|
||||
context.quota_remaining = self._calculate_quota_remaining(user)
|
||||
remaining = self._calculate_balance_remaining(db, user, api_key=api_key)
|
||||
context.balance_remaining = remaining
|
||||
# authorize 可能是异步的,需要检查并 await
|
||||
authorize_start = PerfRecorder.start(force=perf_sampled)
|
||||
try:
|
||||
@@ -238,25 +240,11 @@ class ApiRequestPipeline:
|
||||
request.state.user_id = user.id
|
||||
request.state.api_key_id = api_key.id
|
||||
|
||||
# 检查配额或余额(支持独立Key)
|
||||
quota_ok, message = self.usage_service.check_user_quota(db, user, api_key=api_key)
|
||||
if not quota_ok:
|
||||
# 根据Key类型计算剩余额度
|
||||
if api_key.is_standalone:
|
||||
# 独立Key:显示剩余余额
|
||||
remaining = (
|
||||
None
|
||||
if api_key.current_balance_usd is None
|
||||
else float(api_key.current_balance_usd - (api_key.balance_used_usd or 0))
|
||||
)
|
||||
else:
|
||||
# 普通Key:显示用户配额剩余
|
||||
remaining = (
|
||||
None
|
||||
if user.quota_usd is None or user.quota_usd < 0
|
||||
else float(user.quota_usd - user.used_usd)
|
||||
)
|
||||
raise QuotaExceededException(quota_type="USD", remaining=remaining)
|
||||
# 检查余额(支持独立 Key)
|
||||
access_ok, _message = self.usage_service.check_request_balance(db, user, api_key=api_key)
|
||||
if not access_ok:
|
||||
remaining = self._calculate_balance_remaining(db, user, api_key=api_key)
|
||||
raise BalanceInsufficientException(balance_type="USD", remaining=remaining)
|
||||
|
||||
return user, api_key
|
||||
|
||||
@@ -418,12 +406,15 @@ class ApiRequestPipeline:
|
||||
detail="无效的 Token 格式,需要 Management Token",
|
||||
)
|
||||
|
||||
def _calculate_quota_remaining(self, user: User | None) -> float | None:
|
||||
def _calculate_balance_remaining(
|
||||
self, db: Session, user: User | None, api_key: ApiKey | None = None
|
||||
) -> float | None:
|
||||
if not user:
|
||||
return None
|
||||
if user.quota_usd is None or user.quota_usd < 0:
|
||||
balance = WalletService.get_balance_snapshot(db, user=user, api_key=api_key)
|
||||
if balance is None:
|
||||
return None
|
||||
return max(float(user.quota_usd - user.used_usd), 0.0)
|
||||
return float(balance)
|
||||
|
||||
def _record_audit_event(
|
||||
self,
|
||||
@@ -513,7 +504,7 @@ class ApiRequestPipeline:
|
||||
"request_body_bytes": len(context.raw_body or b""),
|
||||
"has_body": bool(context.raw_body),
|
||||
"request_content_type": request.headers.get("content-type"),
|
||||
"quota_remaining": context.quota_remaining,
|
||||
"balance_remaining": context.balance_remaining,
|
||||
"success": success,
|
||||
# 传递 quiet_logging 标志给审计服务,用于抑制高频轮询日志
|
||||
"quiet_logging": getattr(context, "quiet_logging", False),
|
||||
|
||||
Reference in New Issue
Block a user