""" 管理员 - 兑换码管理 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, )