提供基本前后端骨架
This commit is contained in:
270
app/api/v1/endpoints/balance.py
Normal file
270
app/api/v1/endpoints/balance.py
Normal 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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user