提供基本前后端骨架

This commit is contained in:
hisatri
2026-01-06 23:49:23 +08:00
parent 84d4ccc226
commit 06f8176e23
89 changed files with 19293 additions and 2 deletions

View File

@@ -0,0 +1,270 @@
"""
余额相关 API
包括余额查询、交易记录、兑换等接口。
"""
from fastapi import APIRouter, HTTPException, Query, Request, status
from app.api.deps import ActiveUser, DbSession
from app.core.exceptions import AppException, ValidationError
from app.schemas.base import APIResponse, PaginatedResponse
from app.schemas.balance import (
BalanceResponse,
TransactionResponse,
DeductionRequest,
DeductionResponse,
UNITS_PER_DISPLAY,
format_display,
)
from app.schemas.redeem_code import RedeemRequest, RedeemResponse
from app.services.balance import (
BalanceService,
InsufficientBalanceError,
DuplicateTransactionError,
)
from app.services.redeem_code import (
RedeemCodeService,
RedeemCodeNotFoundError,
RedeemCodeInvalidError,
RedeemCodeExpiredError,
RedeemCodeUsedError,
RedeemCodeDisabledError,
)
from app.models.balance import TransactionType
router = APIRouter()
# ============================================================
# 余额查询
# ============================================================
@router.get(
"",
response_model=APIResponse[BalanceResponse],
summary="获取当前用户余额",
description="获取当前登录用户的余额信息",
)
async def get_my_balance(
current_user: ActiveUser,
session: DbSession,
) -> APIResponse[BalanceResponse]:
"""
获取当前用户余额
返回:
- **balance**: 当前总余额
- **available_balance**: 可用余额(总余额 - 冻结)
- **frozen_balance**: 冻结余额
- **total_recharged**: 累计充值
- **total_consumed**: 累计消费
"""
balance_service = BalanceService(session)
balance = await balance_service.get_balance(current_user.id)
return APIResponse.ok(
data=BalanceResponse(
user_id=balance.user_id,
balance_units=balance.balance,
frozen_units=balance.frozen_balance,
total_recharged_units=balance.total_recharged,
total_consumed_units=balance.total_consumed,
),
)
# ============================================================
# 交易记录
# ============================================================
@router.get(
"/transactions",
response_model=APIResponse[PaginatedResponse[TransactionResponse]],
summary="获取交易记录",
description="获取当前用户的余额交易记录",
)
async def get_my_transactions(
current_user: ActiveUser,
session: DbSession,
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
transaction_type: TransactionType | None = Query(
None, description="交易类型过滤"
),
) -> APIResponse[PaginatedResponse[TransactionResponse]]:
"""
获取交易记录
支持按交易类型过滤:
- **recharge**: 充值
- **deduction**: 扣款
- **refund**: 退款
- **adjustment**: 管理员调整
"""
balance_service = BalanceService(session)
offset = (page - 1) * page_size
transactions, total = await balance_service.get_transactions(
current_user.id,
offset=offset,
limit=page_size,
transaction_type=transaction_type,
)
items = [
TransactionResponse(
id=t.id,
transaction_type=t.transaction_type,
status=t.status,
amount_units=t.amount,
balance_before_units=t.balance_before,
balance_after_units=t.balance_after,
reference_type=t.reference_type,
reference_id=t.reference_id,
description=t.description,
created_at=t.created_at,
)
for t in transactions
]
return APIResponse.ok(
data=PaginatedResponse.create(
items=items,
total=total,
page=page,
page_size=page_size,
),
)
# ============================================================
# 兑换码兑换
# ============================================================
@router.post(
"/redeem",
response_model=APIResponse[RedeemResponse],
summary="兑换余额",
description="使用兑换码充值余额",
)
async def redeem_code(
request: Request,
redeem_request: RedeemRequest,
current_user: ActiveUser,
session: DbSession,
) -> APIResponse[RedeemResponse]:
"""
使用兑换码充值余额
- **code**: 兑换码(格式如 XXXX-XXXX-XXXX-XXXX
返回兑换结果,包括充值金额和余额变化。
"""
redeem_service = RedeemCodeService(session)
# 获取客户端信息
ip_address = request.client.host if request.client else None
user_agent = request.headers.get("user-agent")
try:
result = await redeem_service.redeem(
current_user.id,
redeem_request.code,
ip_address=ip_address,
user_agent=user_agent,
)
return APIResponse.ok(
data=RedeemResponse(
success=True,
message="兑换成功",
face_value=result["face_value"],
balance_before=result["balance_before"],
balance_after=result["balance_after"],
),
message="兑换成功",
)
except RedeemCodeNotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
except (
RedeemCodeInvalidError,
RedeemCodeExpiredError,
RedeemCodeUsedError,
RedeemCodeDisabledError,
) as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=e.message,
)
# ============================================================
# 扣款(内部 API / 服务间调用)
# ============================================================
@router.post(
"/deduct",
response_model=APIResponse[DeductionResponse],
summary="扣款",
description="从当前用户余额中扣款(通常由其他服务调用)",
include_in_schema=True, # 可设为 False 隐藏此 API
)
async def deduct_balance(
deduction_request: DeductionRequest,
current_user: ActiveUser,
session: DbSession,
) -> APIResponse[DeductionResponse]:
"""
扣款
从当前用户余额中扣除指定金额。
- **amount**: 扣款金额(如 1.00
- **reference_type**: 关联业务类型(如 api_call
- **reference_id**: 关联业务 ID
- **description**: 交易描述
- **idempotency_key**: 幂等键(防止重复扣款)
"""
balance_service = BalanceService(session)
try:
transaction = await balance_service.deduct(
current_user.id,
deduction_request.amount_units,
reference_type=deduction_request.reference_type,
reference_id=deduction_request.reference_id,
description=deduction_request.description,
idempotency_key=deduction_request.idempotency_key,
)
return APIResponse.ok(
data=DeductionResponse(
transaction_id=transaction.id,
amount=format_display(abs(transaction.amount)),
balance_before=format_display(transaction.balance_before),
balance_after=format_display(transaction.balance_after),
),
message="扣款成功",
)
except InsufficientBalanceError as e:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail=e.message,
)
except DuplicateTransactionError as e:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=e.message,
)
except ValidationError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=e.message,
)