提供基本前后端骨架
This commit is contained in:
2
app/api/v1/__init__.py
Normal file
2
app/api/v1/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""API v1 模块"""
|
||||
|
||||
2
app/api/v1/endpoints/__init__.py
Normal file
2
app/api/v1/endpoints/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""API v1 端点"""
|
||||
|
||||
6
app/api/v1/endpoints/admin/__init__.py
Normal file
6
app/api/v1/endpoints/admin/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""管理员 API 端点"""
|
||||
|
||||
from app.api.v1.endpoints.admin import redeem_codes
|
||||
|
||||
__all__ = ["redeem_codes"]
|
||||
|
||||
613
app/api/v1/endpoints/admin/redeem_codes.py
Normal file
613
app/api/v1/endpoints/admin/redeem_codes.py
Normal file
@@ -0,0 +1,613 @@
|
||||
"""
|
||||
管理员 - 兑换码管理 API
|
||||
|
||||
包括批量生成、导入导出、查询使用日志等接口。
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.api.deps import SuperUser, DbSession
|
||||
from app.core.exceptions import ResourceNotFoundError, ValidationError
|
||||
from app.schemas.base import APIResponse, PaginatedResponse
|
||||
from app.schemas.balance import (
|
||||
AdminAdjustmentRequest,
|
||||
AdminBalanceResponse,
|
||||
format_display,
|
||||
)
|
||||
from app.schemas.redeem_code import (
|
||||
BatchCreateRequest,
|
||||
BatchResponse,
|
||||
BatchDetailResponse,
|
||||
BatchListResponse,
|
||||
RedeemCodeDetailResponse,
|
||||
RedeemCodeListResponse,
|
||||
BulkImportRequest,
|
||||
BulkImportResponse,
|
||||
ExportResponse,
|
||||
ExportCodeItem,
|
||||
UsageLogResponse,
|
||||
UsageLogListResponse,
|
||||
)
|
||||
from app.services.balance import BalanceService, InsufficientBalanceError
|
||||
from app.services.redeem_code import RedeemCodeService
|
||||
from app.models.redeem_code import RedeemCodeStatus
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 批次管理
|
||||
# ============================================================
|
||||
|
||||
@router.post(
|
||||
"/batches",
|
||||
response_model=APIResponse[BatchResponse],
|
||||
summary="创建兑换码批次",
|
||||
description="批量生成指定数量的兑换码",
|
||||
)
|
||||
async def create_batch(
|
||||
request: BatchCreateRequest,
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[BatchResponse]:
|
||||
"""
|
||||
创建兑换码批次
|
||||
|
||||
批量生成指定数量的兑换码。
|
||||
|
||||
- **name**: 批次名称
|
||||
- **face_value**: 面值(如 10.00)
|
||||
- **count**: 生成数量(最大 10000)
|
||||
- **max_uses**: 每个兑换码最大使用次数
|
||||
- **expires_at**: 过期时间(可选)
|
||||
"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
|
||||
try:
|
||||
batch = await redeem_service.create_batch(
|
||||
name=request.name,
|
||||
face_value_units=request.face_value_units,
|
||||
count=request.count,
|
||||
created_by=current_user.id,
|
||||
description=request.description,
|
||||
max_uses=request.max_uses,
|
||||
expires_at=request.expires_at,
|
||||
)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=BatchResponse(
|
||||
id=batch.id,
|
||||
name=batch.name,
|
||||
description=batch.description,
|
||||
face_value_units=batch.face_value,
|
||||
total_count=batch.total_count,
|
||||
used_count=batch.used_count,
|
||||
created_by=batch.created_by,
|
||||
created_at=batch.created_at,
|
||||
),
|
||||
message=f"成功创建批次,生成 {request.count} 个兑换码",
|
||||
)
|
||||
|
||||
except ValidationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/batches",
|
||||
response_model=APIResponse[BatchListResponse],
|
||||
summary="获取批次列表",
|
||||
description="获取所有兑换码批次",
|
||||
)
|
||||
async def get_batches(
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
) -> APIResponse[BatchListResponse]:
|
||||
"""获取批次列表"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
batches, total = await redeem_service.get_batches(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
)
|
||||
|
||||
items = [
|
||||
BatchResponse(
|
||||
id=b.id,
|
||||
name=b.name,
|
||||
description=b.description,
|
||||
face_value_units=b.face_value,
|
||||
total_count=b.total_count,
|
||||
used_count=b.used_count,
|
||||
created_by=b.created_by,
|
||||
created_at=b.created_at,
|
||||
)
|
||||
for b in batches
|
||||
]
|
||||
|
||||
return APIResponse.ok(
|
||||
data=BatchListResponse.create(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/batches/{batch_id}",
|
||||
response_model=APIResponse[BatchDetailResponse],
|
||||
summary="获取批次详情",
|
||||
description="获取指定批次的详细信息",
|
||||
)
|
||||
async def get_batch_detail(
|
||||
batch_id: str,
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[BatchDetailResponse]:
|
||||
"""获取批次详情"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
|
||||
try:
|
||||
batch = await redeem_service.get_batch_detail(batch_id)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=BatchDetailResponse(
|
||||
id=batch.id,
|
||||
name=batch.name,
|
||||
description=batch.description,
|
||||
face_value_units=batch.face_value,
|
||||
total_count=batch.total_count,
|
||||
used_count=batch.used_count,
|
||||
created_by=batch.created_by,
|
||||
created_at=batch.created_at,
|
||||
),
|
||||
)
|
||||
|
||||
except ResourceNotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 兑换码管理
|
||||
# ============================================================
|
||||
|
||||
@router.get(
|
||||
"/codes",
|
||||
response_model=APIResponse[RedeemCodeListResponse],
|
||||
summary="获取兑换码列表",
|
||||
description="获取所有兑换码,支持多种过滤条件",
|
||||
)
|
||||
async def get_codes(
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
status_filter: RedeemCodeStatus | None = Query(None, alias="status"),
|
||||
batch_id: str | None = Query(None),
|
||||
code: str | None = Query(None, description="兑换码模糊搜索"),
|
||||
created_after: datetime | None = Query(None),
|
||||
created_before: datetime | None = Query(None),
|
||||
) -> APIResponse[RedeemCodeListResponse]:
|
||||
"""
|
||||
获取兑换码列表
|
||||
|
||||
支持过滤:
|
||||
- **status**: 状态(active/used/disabled/expired)
|
||||
- **batch_id**: 批次 ID
|
||||
- **code**: 兑换码模糊搜索
|
||||
- **created_after/created_before**: 创建时间范围
|
||||
"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
codes, total = await redeem_service.get_codes(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
status=status_filter,
|
||||
batch_id=batch_id,
|
||||
code_like=code,
|
||||
created_after=created_after,
|
||||
created_before=created_before,
|
||||
)
|
||||
|
||||
items = [
|
||||
RedeemCodeDetailResponse(
|
||||
id=c.id,
|
||||
code=c.code,
|
||||
face_value_units=c.face_value,
|
||||
status=c.status,
|
||||
max_uses=c.max_uses,
|
||||
used_count=c.used_count,
|
||||
expires_at=c.expires_at,
|
||||
used_at=c.used_at,
|
||||
created_at=c.created_at,
|
||||
batch_id=c.batch_id,
|
||||
batch_name=c.batch.name if c.batch else None,
|
||||
remark=c.remark,
|
||||
created_by=c.created_by,
|
||||
used_by=c.used_by,
|
||||
)
|
||||
for c in codes
|
||||
]
|
||||
|
||||
return APIResponse.ok(
|
||||
data=RedeemCodeListResponse.create(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/codes/{code_id}",
|
||||
response_model=APIResponse[RedeemCodeDetailResponse],
|
||||
summary="获取兑换码详情",
|
||||
)
|
||||
async def get_code_detail(
|
||||
code_id: str,
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[RedeemCodeDetailResponse]:
|
||||
"""获取兑换码详情"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
|
||||
try:
|
||||
code = await redeem_service.get_code_detail(code_id)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=RedeemCodeDetailResponse(
|
||||
id=code.id,
|
||||
code=code.code,
|
||||
face_value_units=code.face_value,
|
||||
status=code.status,
|
||||
max_uses=code.max_uses,
|
||||
used_count=code.used_count,
|
||||
expires_at=code.expires_at,
|
||||
used_at=code.used_at,
|
||||
created_at=code.created_at,
|
||||
batch_id=code.batch_id,
|
||||
batch_name=code.batch.name if code.batch else None,
|
||||
remark=code.remark,
|
||||
created_by=code.created_by,
|
||||
used_by=code.used_by,
|
||||
),
|
||||
)
|
||||
|
||||
except ResourceNotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/codes/{code_id}/disable",
|
||||
response_model=APIResponse[RedeemCodeDetailResponse],
|
||||
summary="禁用兑换码",
|
||||
)
|
||||
async def disable_code(
|
||||
code_id: str,
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[RedeemCodeDetailResponse]:
|
||||
"""禁用指定兑换码"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
|
||||
try:
|
||||
code = await redeem_service.disable_code(code_id)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=RedeemCodeDetailResponse(
|
||||
id=code.id,
|
||||
code=code.code,
|
||||
face_value_units=code.face_value,
|
||||
status=code.status,
|
||||
max_uses=code.max_uses,
|
||||
used_count=code.used_count,
|
||||
expires_at=code.expires_at,
|
||||
used_at=code.used_at,
|
||||
created_at=code.created_at,
|
||||
batch_id=code.batch_id,
|
||||
remark=code.remark,
|
||||
created_by=code.created_by,
|
||||
used_by=code.used_by,
|
||||
),
|
||||
message="兑换码已禁用",
|
||||
)
|
||||
|
||||
except ResourceNotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/codes/{code_id}/enable",
|
||||
response_model=APIResponse[RedeemCodeDetailResponse],
|
||||
summary="启用兑换码",
|
||||
)
|
||||
async def enable_code(
|
||||
code_id: str,
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[RedeemCodeDetailResponse]:
|
||||
"""重新启用指定兑换码"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
|
||||
try:
|
||||
code = await redeem_service.enable_code(code_id)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=RedeemCodeDetailResponse(
|
||||
id=code.id,
|
||||
code=code.code,
|
||||
face_value_units=code.face_value,
|
||||
status=code.status,
|
||||
max_uses=code.max_uses,
|
||||
used_count=code.used_count,
|
||||
expires_at=code.expires_at,
|
||||
used_at=code.used_at,
|
||||
created_at=code.created_at,
|
||||
batch_id=code.batch_id,
|
||||
remark=code.remark,
|
||||
created_by=code.created_by,
|
||||
used_by=code.used_by,
|
||||
),
|
||||
message="兑换码已启用",
|
||||
)
|
||||
|
||||
except ResourceNotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 导入导出
|
||||
# ============================================================
|
||||
|
||||
@router.post(
|
||||
"/codes/import",
|
||||
response_model=APIResponse[BulkImportResponse],
|
||||
summary="批量导入兑换码",
|
||||
description="批量导入自定义兑换码",
|
||||
)
|
||||
async def import_codes(
|
||||
request: BulkImportRequest,
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[BulkImportResponse]:
|
||||
"""
|
||||
批量导入兑换码
|
||||
|
||||
可以导入自定义格式的兑换码。
|
||||
|
||||
- **codes**: 兑换码列表
|
||||
- **batch_name**: 批次名称(可选)
|
||||
"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
|
||||
# 转换请求数据
|
||||
codes_data = [
|
||||
{
|
||||
"code": c.code,
|
||||
"face_value_units": c.face_value_units,
|
||||
"max_uses": c.max_uses,
|
||||
"expires_at": c.expires_at,
|
||||
"remark": c.remark,
|
||||
}
|
||||
for c in request.codes
|
||||
]
|
||||
|
||||
result = await redeem_service.import_codes(
|
||||
codes_data,
|
||||
created_by=current_user.id,
|
||||
batch_name=request.batch_name,
|
||||
)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=BulkImportResponse(
|
||||
success_count=result["success_count"],
|
||||
failed_count=result["failed_count"],
|
||||
failed_codes=result["failed_codes"],
|
||||
batch_id=result["batch_id"],
|
||||
),
|
||||
message=f"导入完成:成功 {result['success_count']},失败 {result['failed_count']}",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/codes/export",
|
||||
response_model=APIResponse[ExportResponse],
|
||||
summary="导出兑换码",
|
||||
description="导出兑换码数据",
|
||||
)
|
||||
async def export_codes(
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
batch_id: str | None = Query(None),
|
||||
status_filter: RedeemCodeStatus | None = Query(None, alias="status"),
|
||||
limit: int = Query(10000, ge=1, le=50000),
|
||||
) -> APIResponse[ExportResponse]:
|
||||
"""
|
||||
导出兑换码
|
||||
|
||||
支持按批次或状态过滤。
|
||||
"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
|
||||
codes = await redeem_service.export_codes(
|
||||
batch_id=batch_id,
|
||||
status=status_filter,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
items = [
|
||||
ExportCodeItem(
|
||||
code=c["code"],
|
||||
face_value=c["face_value"],
|
||||
status=c["status"],
|
||||
max_uses=c["max_uses"],
|
||||
used_count=c["used_count"],
|
||||
expires_at=c["expires_at"],
|
||||
created_at=c["created_at"],
|
||||
used_at=c["used_at"],
|
||||
used_by=c["used_by"],
|
||||
)
|
||||
for c in codes
|
||||
]
|
||||
|
||||
return APIResponse.ok(
|
||||
data=ExportResponse(
|
||||
total=len(items),
|
||||
codes=items,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 使用日志
|
||||
# ============================================================
|
||||
|
||||
@router.get(
|
||||
"/usage-logs",
|
||||
response_model=APIResponse[UsageLogListResponse],
|
||||
summary="获取使用日志",
|
||||
description="查询兑换码使用记录",
|
||||
)
|
||||
async def get_usage_logs(
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
redeem_code_id: str | None = Query(None),
|
||||
user_id: str | None = Query(None),
|
||||
code: str | None = Query(None, description="兑换码模糊搜索"),
|
||||
created_after: datetime | None = Query(None),
|
||||
created_before: datetime | None = Query(None),
|
||||
) -> APIResponse[UsageLogListResponse]:
|
||||
"""
|
||||
获取使用日志
|
||||
|
||||
支持过滤:
|
||||
- **redeem_code_id**: 兑换码 ID
|
||||
- **user_id**: 用户 ID
|
||||
- **code**: 兑换码模糊搜索
|
||||
- **created_after/created_before**: 使用时间范围
|
||||
"""
|
||||
redeem_service = RedeemCodeService(session)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
logs, total = await redeem_service.get_usage_logs(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
redeem_code_id=redeem_code_id,
|
||||
user_id=user_id,
|
||||
code_like=code,
|
||||
created_after=created_after,
|
||||
created_before=created_before,
|
||||
)
|
||||
|
||||
items = [
|
||||
UsageLogResponse(
|
||||
id=log.id,
|
||||
redeem_code_id=log.redeem_code_id,
|
||||
code_snapshot=log.code_snapshot,
|
||||
user_id=log.user_id,
|
||||
username=log.user.username if log.user else None,
|
||||
face_value=format_display(log.face_value),
|
||||
ip_address=log.ip_address,
|
||||
created_at=log.created_at,
|
||||
)
|
||||
for log in logs
|
||||
]
|
||||
|
||||
return APIResponse.ok(
|
||||
data=UsageLogListResponse.create(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 余额管理
|
||||
# ============================================================
|
||||
|
||||
@router.post(
|
||||
"/balance/adjust",
|
||||
response_model=APIResponse[AdminBalanceResponse],
|
||||
summary="调整用户余额",
|
||||
description="管理员手动调整用户余额",
|
||||
)
|
||||
async def adjust_balance(
|
||||
request: AdminAdjustmentRequest,
|
||||
current_user: SuperUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[AdminBalanceResponse]:
|
||||
"""
|
||||
调整用户余额
|
||||
|
||||
- **user_id**: 目标用户 ID
|
||||
- **amount**: 调整金额(正数增加,负数减少)
|
||||
- **reason**: 调整原因
|
||||
"""
|
||||
balance_service = BalanceService(session)
|
||||
|
||||
try:
|
||||
transaction = await balance_service.admin_adjust(
|
||||
request.user_id,
|
||||
request.amount_units,
|
||||
operator_id=current_user.id,
|
||||
reason=request.reason,
|
||||
)
|
||||
|
||||
# 获取更新后的余额
|
||||
balance = await balance_service.get_balance(request.user_id)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=AdminBalanceResponse(
|
||||
user_id=balance.user_id,
|
||||
username=balance.user.username if balance.user else "",
|
||||
balance=format_display(balance.balance),
|
||||
available_balance=format_display(balance.available_balance),
|
||||
frozen_balance=format_display(balance.frozen_balance),
|
||||
total_recharged=format_display(balance.total_recharged),
|
||||
total_consumed=format_display(balance.total_consumed),
|
||||
version=balance.version,
|
||||
created_at=balance.created_at,
|
||||
updated_at=balance.updated_at,
|
||||
),
|
||||
message=f"余额调整成功,变动 {request.amount:+.2f}",
|
||||
)
|
||||
|
||||
except InsufficientBalanceError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=e.message,
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
191
app/api/v1/endpoints/auth.py
Normal file
191
app/api/v1/endpoints/auth.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
认证相关 API
|
||||
|
||||
包括注册、登录、退出、刷新令牌、修改密码等接口。
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import ActiveUser, DbSession, get_auth_service
|
||||
from app.core.exceptions import (
|
||||
InvalidCredentialsError,
|
||||
PasswordValidationError,
|
||||
ResourceConflictError,
|
||||
TokenError,
|
||||
UserDisabledError,
|
||||
)
|
||||
from app.schemas.auth import (
|
||||
LoginRequest,
|
||||
PasswordChangeRequest,
|
||||
RefreshTokenRequest,
|
||||
TokenResponse,
|
||||
)
|
||||
from app.schemas.base import APIResponse
|
||||
from app.schemas.user import UserCreate, UserResponse
|
||||
from app.services.auth import AuthService
|
||||
from app.services.user import UserService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/register",
|
||||
response_model=APIResponse[UserResponse],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="用户注册",
|
||||
description="创建新用户账户",
|
||||
)
|
||||
async def register(
|
||||
user_data: UserCreate,
|
||||
session: DbSession,
|
||||
) -> APIResponse[UserResponse]:
|
||||
"""
|
||||
用户注册接口
|
||||
|
||||
- **username**: 用户名(字母开头,3-32位)
|
||||
- **email**: 邮箱(可选)
|
||||
- **password**: 密码(8位以上,需包含大小写字母和数字)
|
||||
- **nickname**: 昵称(可选)
|
||||
"""
|
||||
user_service = UserService(session)
|
||||
|
||||
try:
|
||||
user = await user_service.create_user(user_data)
|
||||
return APIResponse.ok(
|
||||
data=UserResponse.model_validate(user),
|
||||
message="注册成功",
|
||||
)
|
||||
except ResourceConflictError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/login",
|
||||
response_model=APIResponse[TokenResponse],
|
||||
summary="用户登录",
|
||||
description="使用用户名/邮箱和密码登录",
|
||||
)
|
||||
async def login(
|
||||
login_data: LoginRequest,
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
) -> APIResponse[TokenResponse]:
|
||||
"""
|
||||
用户登录接口
|
||||
|
||||
- **username**: 用户名或邮箱
|
||||
- **password**: 密码
|
||||
|
||||
返回访问令牌和刷新令牌。
|
||||
"""
|
||||
try:
|
||||
_, tokens = await auth_service.login(
|
||||
username=login_data.username,
|
||||
password=login_data.password,
|
||||
)
|
||||
return APIResponse.ok(data=tokens, message="登录成功")
|
||||
except InvalidCredentialsError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=e.message,
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
except UserDisabledError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/logout",
|
||||
response_model=APIResponse[None],
|
||||
summary="用户退出",
|
||||
description="退出登录(客户端应删除本地令牌)",
|
||||
)
|
||||
async def logout(
|
||||
current_user: ActiveUser,
|
||||
) -> APIResponse[None]:
|
||||
"""
|
||||
用户退出接口
|
||||
|
||||
由于使用的是无状态 JWT,服务端不存储令牌,
|
||||
因此退出登录主要由客户端删除本地存储的令牌实现。
|
||||
|
||||
如果需要实现令牌黑名单,可以在后续版本中添加。
|
||||
"""
|
||||
# 可以在这里添加令牌黑名单逻辑
|
||||
# 或者记录退出日志
|
||||
return APIResponse.ok(message="退出成功")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/refresh",
|
||||
response_model=APIResponse[TokenResponse],
|
||||
summary="刷新令牌",
|
||||
description="使用刷新令牌获取新的访问令牌",
|
||||
)
|
||||
async def refresh_token(
|
||||
token_data: RefreshTokenRequest,
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
) -> APIResponse[TokenResponse]:
|
||||
"""
|
||||
刷新令牌接口
|
||||
|
||||
使用刷新令牌获取新的访问令牌和刷新令牌。
|
||||
"""
|
||||
try:
|
||||
tokens = await auth_service.refresh_tokens(token_data.refresh_token)
|
||||
return APIResponse.ok(data=tokens, message="刷新成功")
|
||||
except TokenError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=e.message,
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
except UserDisabledError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/change-password",
|
||||
response_model=APIResponse[None],
|
||||
summary="修改密码",
|
||||
description="修改当前用户密码",
|
||||
)
|
||||
async def change_password(
|
||||
password_data: PasswordChangeRequest,
|
||||
current_user: ActiveUser,
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
) -> APIResponse[None]:
|
||||
"""
|
||||
修改密码接口
|
||||
|
||||
- **current_password**: 当前密码
|
||||
- **new_password**: 新密码
|
||||
"""
|
||||
try:
|
||||
await auth_service.change_password(
|
||||
user_id=current_user.id,
|
||||
password_data=password_data,
|
||||
)
|
||||
return APIResponse.ok(message="密码修改成功")
|
||||
except InvalidCredentialsError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=e.message,
|
||||
)
|
||||
except PasswordValidationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
157
app/api/v1/endpoints/oauth2.py
Normal file
157
app/api/v1/endpoints/oauth2.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
OAuth2 认证 API
|
||||
|
||||
提供 OAuth2 第三方登录功能。
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import DbSession
|
||||
from app.core.config import settings
|
||||
from app.core.exceptions import AuthenticationError
|
||||
from app.schemas.base import APIResponse
|
||||
from app.schemas.oauth2 import OAuth2AuthorizeResponse, OAuth2LoginResponse
|
||||
from app.services.oauth2 import OAuth2Service, OAuth2StateError
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def get_oauth2_service(session: DbSession) -> OAuth2Service:
|
||||
"""获取 OAuth2 服务实例"""
|
||||
return OAuth2Service(session)
|
||||
|
||||
|
||||
def _get_redirect_uri(request: Request) -> str:
|
||||
"""
|
||||
获取 OAuth2 回调 URL
|
||||
|
||||
根据请求动态构建完整的回调 URL
|
||||
"""
|
||||
# 优先使用 X-Forwarded 头(反向代理场景)
|
||||
scheme = request.headers.get("X-Forwarded-Proto", request.url.scheme)
|
||||
host = request.headers.get("X-Forwarded-Host", request.url.netloc)
|
||||
|
||||
return f"{scheme}://{host}{settings.oauth2_callback_path}"
|
||||
|
||||
|
||||
@router.get(
|
||||
"/authorize",
|
||||
response_model=APIResponse[OAuth2AuthorizeResponse],
|
||||
summary="获取 OAuth2 授权 URL",
|
||||
description="获取第三方登录授权页面 URL",
|
||||
)
|
||||
async def get_authorize_url(
|
||||
request: Request,
|
||||
session: DbSession,
|
||||
) -> APIResponse[OAuth2AuthorizeResponse]:
|
||||
"""
|
||||
获取 OAuth2 授权 URL
|
||||
|
||||
返回授权页面 URL 和状态码,客户端应重定向用户到该 URL。
|
||||
|
||||
流程:
|
||||
1. 前端调用此接口获取授权 URL
|
||||
2. 前端将用户重定向到授权 URL
|
||||
3. 用户在第三方平台完成授权
|
||||
4. 第三方平台重定向回回调 URL,携带 code 和 state
|
||||
5. 前端调用 /callback 接口完成登录
|
||||
"""
|
||||
# 检查 OAuth2 是否已配置
|
||||
if not settings.oauth2_client_id or not settings.oauth2_client_secret:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="OAuth2 登录未配置",
|
||||
)
|
||||
|
||||
oauth2_service = OAuth2Service(session)
|
||||
redirect_uri = _get_redirect_uri(request)
|
||||
|
||||
authorize_url, state = oauth2_service.generate_authorize_url(redirect_uri)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=OAuth2AuthorizeResponse(
|
||||
authorize_url=authorize_url,
|
||||
state=state,
|
||||
),
|
||||
message="请重定向到授权 URL",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/callback",
|
||||
response_model=APIResponse[OAuth2LoginResponse],
|
||||
summary="OAuth2 回调",
|
||||
description="处理 OAuth2 授权回调,完成登录",
|
||||
)
|
||||
async def oauth2_callback(
|
||||
request: Request,
|
||||
code: Annotated[str, Query(description="授权码")],
|
||||
state: Annotated[str, Query(description="状态码")],
|
||||
session: DbSession,
|
||||
) -> APIResponse[OAuth2LoginResponse]:
|
||||
"""
|
||||
OAuth2 回调接口
|
||||
|
||||
处理第三方平台的授权回调:
|
||||
1. 验证 state 防止 CSRF 攻击
|
||||
2. 用 code 换取访问令牌
|
||||
3. 获取用户信息
|
||||
4. 创建或关联本地用户
|
||||
5. 返回 JWT 令牌
|
||||
"""
|
||||
oauth2_service = OAuth2Service(session)
|
||||
redirect_uri = _get_redirect_uri(request)
|
||||
|
||||
try:
|
||||
user, tokens, is_new_user = await oauth2_service.authenticate(
|
||||
code=code,
|
||||
state=state,
|
||||
redirect_uri=redirect_uri,
|
||||
)
|
||||
|
||||
return APIResponse.ok(
|
||||
data=OAuth2LoginResponse(
|
||||
access_token=tokens.access_token,
|
||||
refresh_token=tokens.refresh_token,
|
||||
token_type=tokens.token_type,
|
||||
expires_in=tokens.expires_in,
|
||||
is_new_user=is_new_user,
|
||||
),
|
||||
message="登录成功" if not is_new_user else "注册并登录成功",
|
||||
)
|
||||
|
||||
except OAuth2StateError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=e.message,
|
||||
)
|
||||
except AuthenticationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/callback",
|
||||
response_model=APIResponse[OAuth2LoginResponse],
|
||||
summary="OAuth2 回调 (POST)",
|
||||
description="处理 OAuth2 授权回调(POST 方式)",
|
||||
)
|
||||
async def oauth2_callback_post(
|
||||
request: Request,
|
||||
code: Annotated[str, Query(description="授权码")],
|
||||
state: Annotated[str, Query(description="状态码")],
|
||||
session: DbSession,
|
||||
) -> APIResponse[OAuth2LoginResponse]:
|
||||
"""
|
||||
OAuth2 回调接口(POST 方式)
|
||||
|
||||
某些场景下前端可能使用 POST 方式调用回调。
|
||||
逻辑与 GET 方式相同。
|
||||
"""
|
||||
return await oauth2_callback(request, code, state, session)
|
||||
|
||||
103
app/api/v1/endpoints/users.py
Normal file
103
app/api/v1/endpoints/users.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
用户相关 API
|
||||
|
||||
包括获取用户信息、更新用户资料等接口。
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from app.api.deps import ActiveUser, DbSession
|
||||
from app.core.exceptions import ResourceConflictError, UserNotFoundError
|
||||
from app.schemas.base import APIResponse
|
||||
from app.schemas.user import UserResponse, UserUpdate
|
||||
from app.services.user import UserService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me",
|
||||
response_model=APIResponse[UserResponse],
|
||||
summary="获取当前用户信息",
|
||||
description="获取当前登录用户的详细信息",
|
||||
)
|
||||
async def get_current_user_info(
|
||||
current_user: ActiveUser,
|
||||
) -> APIResponse[UserResponse]:
|
||||
"""
|
||||
获取当前用户信息
|
||||
|
||||
返回当前登录用户的完整信息。
|
||||
"""
|
||||
return APIResponse.ok(
|
||||
data=UserResponse.model_validate(current_user),
|
||||
)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/me",
|
||||
response_model=APIResponse[UserResponse],
|
||||
summary="更新当前用户信息",
|
||||
description="更新当前登录用户的资料",
|
||||
)
|
||||
async def update_current_user(
|
||||
update_data: UserUpdate,
|
||||
current_user: ActiveUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[UserResponse]:
|
||||
"""
|
||||
更新当前用户信息
|
||||
|
||||
支持更新:
|
||||
- **nickname**: 昵称
|
||||
- **email**: 邮箱
|
||||
- **avatar_url**: 头像 URL
|
||||
- **bio**: 个人简介
|
||||
"""
|
||||
user_service = UserService(session)
|
||||
|
||||
try:
|
||||
user = await user_service.update_user(
|
||||
user_id=current_user.id,
|
||||
update_data=update_data,
|
||||
)
|
||||
return APIResponse.ok(
|
||||
data=UserResponse.model_validate(user),
|
||||
message="更新成功",
|
||||
)
|
||||
except ResourceConflictError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=e.message,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{user_id}",
|
||||
response_model=APIResponse[UserResponse],
|
||||
summary="获取指定用户信息",
|
||||
description="获取指定用户的公开信息",
|
||||
)
|
||||
async def get_user_by_id(
|
||||
user_id: str,
|
||||
current_user: ActiveUser,
|
||||
session: DbSession,
|
||||
) -> APIResponse[UserResponse]:
|
||||
"""
|
||||
获取指定用户信息
|
||||
|
||||
- **user_id**: 用户 ID
|
||||
"""
|
||||
user_service = UserService(session)
|
||||
|
||||
try:
|
||||
user = await user_service.get_user_by_id(user_id)
|
||||
return APIResponse.ok(
|
||||
data=UserResponse.model_validate(user),
|
||||
)
|
||||
except UserNotFoundError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在",
|
||||
)
|
||||
|
||||
45
app/api/v1/router.py
Normal file
45
app/api/v1/router.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
API v1 路由聚合
|
||||
|
||||
汇总所有 v1 版本的 API 路由。
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.endpoints import auth, oauth2, users, balance
|
||||
from app.api.v1.endpoints.admin import redeem_codes as admin_redeem_codes
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# 注册子路由
|
||||
api_router.include_router(
|
||||
auth.router,
|
||||
prefix="/auth",
|
||||
tags=["认证"],
|
||||
)
|
||||
|
||||
api_router.include_router(
|
||||
oauth2.router,
|
||||
prefix="/auth/oauth2",
|
||||
tags=["OAuth2 登录"],
|
||||
)
|
||||
|
||||
api_router.include_router(
|
||||
users.router,
|
||||
prefix="/users",
|
||||
tags=["用户"],
|
||||
)
|
||||
|
||||
api_router.include_router(
|
||||
balance.router,
|
||||
prefix="/balance",
|
||||
tags=["余额"],
|
||||
)
|
||||
|
||||
# 管理员路由
|
||||
api_router.include_router(
|
||||
admin_redeem_codes.router,
|
||||
prefix="/admin/redeem-codes",
|
||||
tags=["管理员 - 兑换码"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user