import logging from django.http import HttpResponse from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from rest_framework.authentication import SessionAuthentication from rest_framework.exceptions import AuthenticationFailed from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework import status from rest_framework.views import APIView from apps.api.authentication import ApiKeyAuthentication from apps.api.errors import api_error from apps.api.generation import ( ApiRequestError, generate_image_response, generate_title_response, ) from apps.api.serializers import ( GenerateImageRequestSerializer, GenerateTitleRequestSerializer, RechargeCreateRequestSerializer, RechargeStatusRequestSerializer, ) from apps.api.throttles import GenerateRateThrottle, throttle_api_auth_failure from apps.ai.catalog import get_public_model_catalog from apps.billing.models import RechargeOrder from apps.billing.payment_gateways import ( PaymentOrderCreateError, PaymentQueryUnavailableError, PaymentVerificationError, query_payment_order, verify_alipay_callback, verify_wechat_callback, ) from apps.billing.pricing import NoExchangeRateError from apps.billing.services import ( InvalidRechargeOrderStateError, RechargeAmountMismatchError, RechargeCallbackError, RechargeOrderCreateError, RechargeOrderNotFoundError, RechargePayMethodMismatchError, apply_recharge_payment, create_recharge_order, get_balance_snapshot, query_and_apply_recharge_payment, ) from apps.portal.models import DownloadRelease logger = logging.getLogger(__name__) class ExternalApiView(APIView): authentication_classes = (ApiKeyAuthentication,) permission_classes = (IsAuthenticated,) def permission_denied(self, request, message=None, code=None): if request.authenticators and not request.successful_authenticator: throttle_api_auth_failure(request) raise AuthenticationFailed(api_error("unauthorized", "缺失或无效 API Key")) super().permission_denied(request, message=message, code=code) class GenerateTitleView(ExternalApiView): throttle_classes = (GenerateRateThrottle,) def post(self, request): serializer = GenerateTitleRequestSerializer(data=request.data) if not serializer.is_valid(): return Response( api_error("bad_request", "参数错误"), status=status.HTTP_400_BAD_REQUEST, ) try: data = generate_title_response( user=request.user, api_key=request.auth, request_data=serializer.validated_data, ) except ApiRequestError as exc: return Response(exc.as_response_data(), status=exc.http_status) return Response(data, status=status.HTTP_200_OK) class GenerateImageView(ExternalApiView): throttle_classes = (GenerateRateThrottle,) def post(self, request): serializer = GenerateImageRequestSerializer(data=request.data) if not serializer.is_valid(): return Response( api_error("bad_request", "参数错误"), status=status.HTTP_400_BAD_REQUEST, ) try: data = generate_image_response( user=request.user, api_key=request.auth, request=request, request_data=serializer.validated_data, ) except ApiRequestError as exc: return Response(exc.as_response_data(), status=exc.http_status) return Response(data, status=status.HTTP_200_OK) class BalanceView(ExternalApiView): def get(self, request): balance = get_balance_snapshot(request.user) username = request.user.get_username() display_name = request.user.get_full_name() or username return Response( { "user": username, "points_balance": balance.points_balance, "account": { "username": username, "display_name": display_name, }, }, status=status.HTTP_200_OK, ) class ModelsView(ExternalApiView): def get(self, request): return Response( {"models": get_public_model_catalog()}, status=status.HTTP_200_OK, ) def _release_unpublished_response(platform: str) -> dict: return { "platform": platform, "release": None, "message": "暂未发布", } def _absolute_download_url(request, release: DownloadRelease) -> str: download_url = release.download_url if not download_url: return "" if download_url.startswith(("http://", "https://")): return download_url return request.build_absolute_uri(download_url) class ClientLatestReleaseView(APIView): authentication_classes = () permission_classes = (AllowAny,) def get(self, request): platform = ( request.query_params.get("platform") or DownloadRelease.Platform.WINDOWS ).strip().lower() if platform not in DownloadRelease.Platform.values: return Response( api_error("bad_request", "参数错误"), status=status.HTTP_400_BAD_REQUEST, ) release = ( DownloadRelease.objects.filter(platform=platform, is_current=True) .order_by("-created_at", "-id") .first() ) if release is None: return Response( _release_unpublished_response(platform), status=status.HTTP_200_OK, ) download_url = _absolute_download_url(request, release) if not download_url: return Response( _release_unpublished_response(platform), status=status.HTTP_200_OK, ) return Response( { "platform": platform, "release": { "version": release.version, "download_url": download_url, "sha256": release.sha256, "release_notes": release.release_notes, "force_update": release.force_update, "published_at": timezone.localtime(release.updated_at).isoformat(), }, }, status=status.HTTP_200_OK, ) class PortalSessionApiView(APIView): authentication_classes = (SessionAuthentication,) permission_classes = (IsAuthenticated,) def _recharge_order_response(order: RechargeOrder) -> dict: is_expired = bool( order.status == RechargeOrder.Status.PENDING and order.expires_at is not None and order.expires_at <= timezone.now() ) return { "order_no": order.order_no, "amount": f"{order.amount_money:.2f}", "currency": order.currency, "exchange_rate": f"{order.exchange_rate:.4f}", "points_granted": order.points_granted, "pay_method": order.pay_method, "status": order.status, "code_url": order.code_url, "expires_at": order.expires_at.isoformat() if order.expires_at else None, "paid_at": order.paid_at.isoformat() if order.paid_at else None, "is_expired": is_expired, } class RechargeCreateView(PortalSessionApiView): def post(self, request): serializer = RechargeCreateRequestSerializer(data=request.data) if not serializer.is_valid(): return Response( api_error("bad_request", "参数错误"), status=status.HTTP_400_BAD_REQUEST, ) try: order = create_recharge_order( user=request.user, amount=serializer.validated_data["amount"], pay_method=serializer.validated_data["pay_method"], ) except NoExchangeRateError: return Response( api_error("no_exchange_rate", "未配置当前币种汇率"), status=status.HTTP_400_BAD_REQUEST, ) except RechargeOrderCreateError: return Response( api_error("bad_request", "充值下单参数错误"), status=status.HTTP_400_BAD_REQUEST, ) except PaymentOrderCreateError: return Response( api_error("payment_order_create_failed", "支付下单失败"), status=status.HTTP_502_BAD_GATEWAY, ) return Response(_recharge_order_response(order), status=status.HTTP_201_CREATED) class RechargeStatusView(PortalSessionApiView): def get(self, request): serializer = RechargeStatusRequestSerializer(data=request.query_params) if not serializer.is_valid(): return Response( api_error("bad_request", "参数错误"), status=status.HTTP_400_BAD_REQUEST, ) order_no = serializer.validated_data["order_no"] order = RechargeOrder.objects.filter( user=request.user, order_no=order_no, ).first() if order is None: return Response( api_error("order_not_found", "充值订单不存在"), status=status.HTTP_404_NOT_FOUND, ) if order.status == RechargeOrder.Status.PENDING: try: result = query_and_apply_recharge_payment(order.order_no, query_payment_order) order = result.order except (PaymentQueryUnavailableError, PaymentOrderCreateError): order.refresh_from_db() except RechargeCallbackError as exc: logger.warning( "Rejected active recharge query result for %s: %s", order.order_no, exc.__class__.__name__, ) return Response( api_error(exc.code, "支付查单结果与本地订单不一致"), status=status.HTTP_400_BAD_REQUEST, ) except Exception as exc: # pragma: no cover - external gateway/runtime. logger.warning( "Active recharge query failed for %s: %s", order.order_no, exc.__class__.__name__, ) order.refresh_from_db() return Response(_recharge_order_response(order), status=status.HTTP_200_OK) class RechargeCallbackView(APIView): authentication_classes = () permission_classes = () @staticmethod def _error_response(exc: Exception): if isinstance(exc, PaymentVerificationError): return ( api_error("signature_invalid", "支付回调验签失败"), status.HTTP_400_BAD_REQUEST, ) if isinstance(exc, RechargeAmountMismatchError): return ( api_error("amount_mismatch", "支付回调金额与本地订单金额不一致"), status.HTTP_400_BAD_REQUEST, ) if isinstance(exc, RechargeCallbackError): return api_error(exc.code, "支付回调处理失败"), status.HTTP_400_BAD_REQUEST return api_error("bad_request", "支付回调处理失败"), status.HTTP_400_BAD_REQUEST @method_decorator(csrf_exempt, name="dispatch") class WechatRechargeCallbackView(RechargeCallbackView): def post(self, request): try: payment = verify_wechat_callback(request.headers, request.body) apply_recharge_payment(payment) except ( PaymentVerificationError, RechargeAmountMismatchError, InvalidRechargeOrderStateError, RechargeOrderNotFoundError, RechargePayMethodMismatchError, ) as exc: logger.warning("Rejected WeChat recharge callback: %s", exc.__class__.__name__) data, http_status = self._error_response(exc) return Response(data, status=http_status) return Response({"code": "SUCCESS", "message": "成功"}, status=status.HTTP_200_OK) @method_decorator(csrf_exempt, name="dispatch") class AlipayRechargeCallbackView(RechargeCallbackView): def post(self, request): try: payload = request.data.dict() if hasattr(request.data, "dict") else dict(request.data) payment = verify_alipay_callback(payload) apply_recharge_payment(payment) except ( PaymentVerificationError, RechargeAmountMismatchError, InvalidRechargeOrderStateError, RechargeOrderNotFoundError, RechargePayMethodMismatchError, ) as exc: logger.warning("Rejected Alipay recharge callback: %s", exc.__class__.__name__) return HttpResponse("fail", status=status.HTTP_400_BAD_REQUEST) return HttpResponse("success", content_type="text/plain", status=status.HTTP_200_OK)