feat(wallet): 钱包系统替代配额系统,新增支付与退款机制

- 新增钱包余额管理、充值、扣费、退款完整流程
- 新增支付网关抽象层(支持手动/支付宝/微信)
- 用量计费从配额系统迁移到钱包余额扣费
- 新增管理员钱包管理与支付订单管理页面
- 新增用户钱包中心页面
- 移除独立 Key 锁定机制,统一由钱包余额控制
- 新增相关 API 路由、序列化器与数据库迁移
- 新增钱包、支付、退款相关测试
This commit is contained in:
LewisPen
2026-03-08 00:05:48 +08:00
committed by fawney19
parent 9cdcce1b5f
commit 783f654953
108 changed files with 13152 additions and 3372 deletions

View File

@@ -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

View File

@@ -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),