from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.core.paginator import Paginator from django.db.models import Sum from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy from django.views import View from django.views.generic import FormView, TemplateView from apps.ai.catalog import get_public_model_catalog from apps.billing.payment_gateways import PaymentOrderCreateError, payment_callback_mode from apps.billing.pricing import NoExchangeRateError from apps.billing.models import PointsLedger, RechargeOrder from apps.billing.services import ( RechargeOrderCreateError, create_recharge_order, get_balance_snapshot, ) from apps.users.models import ApiKey from .forms import ApiKeyCreateForm, RechargeCreateForm from .models import DownloadRelease NEW_API_KEY_SESSION_KEY = "portal_new_api_key" RECORDS_PAGE_SIZE = 50 def _sum_or_zero(queryset, field_name: str): return queryset.aggregate(total=Sum(field_name))["total"] or 0 def get_portal_account_summary(user) -> dict: paid_orders = RechargeOrder.objects.filter( user=user, status=RechargeOrder.Status.PAID, ) consume_entries = PointsLedger.objects.filter( user=user, change_type=PointsLedger.ChangeType.CONSUME, ) refund_entries = PointsLedger.objects.filter( user=user, change_type=PointsLedger.ChangeType.REFUND, ) recharge_entries = PointsLedger.objects.filter( user=user, change_type=PointsLedger.ChangeType.RECHARGE, ) consumed_points = abs(int(_sum_or_zero(consume_entries, "points_delta"))) refunded_points = int(_sum_or_zero(refund_entries, "points_delta")) return { "recharge_total_amount": _sum_or_zero(paid_orders, "amount_money"), "recharge_points_total": int(_sum_or_zero(recharge_entries, "points_delta")), "consumed_points_total": consumed_points, "refunded_points_total": refunded_points, "net_used_points": consumed_points - refunded_points, "paid_recharge_count": paid_orders.count(), } def get_recharge_orders_for_user(user): return RechargeOrder.objects.filter(user=user).order_by("-created_at", "-id") def get_usage_ledger_entries_for_user(user): return ( PointsLedger.objects.filter( user=user, change_type__in=( PointsLedger.ChangeType.CONSUME, PointsLedger.ChangeType.REFUND, ), ) .select_related("ref_call", "ref_call__api_key") .order_by("-created_at", "-id") ) def paginate_records(request, queryset): paginator = Paginator(queryset, RECORDS_PAGE_SIZE) page_obj = paginator.get_page(request.GET.get("page")) return { "paginator": paginator, "page_obj": page_obj, "is_paginated": page_obj.has_other_pages(), "records_total_count": paginator.count, "records_page_size": RECORDS_PAGE_SIZE, } class HomeView(TemplateView): template_name = "portal/home.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["current_release"] = ( DownloadRelease.objects.filter( platform=DownloadRelease.Platform.WINDOWS, is_current=True, ) .order_by("-created_at", "-id") .first() ) return context class DashboardView(LoginRequiredMixin, TemplateView): template_name = "portal/dashboard.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["balance"] = get_balance_snapshot(self.request.user) context.update(get_portal_account_summary(self.request.user)) context["recent_recharge_orders"] = get_recharge_orders_for_user( self.request.user )[:5] context["recent_usage_entries"] = get_usage_ledger_entries_for_user( self.request.user )[:5] return context class ApiKeyListCreateView(LoginRequiredMixin, FormView): template_name = "portal/apikeys.html" form_class = ApiKeyCreateForm success_url = reverse_lazy("portal-apikeys") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["api_keys"] = ApiKey.objects.filter(user=self.request.user).order_by( "-created_at", "-id", ) context["new_api_key"] = self.request.session.pop(NEW_API_KEY_SESSION_KEY, None) return context def form_valid(self, form): name = form.cleaned_data["name"].strip() api_key, raw_key = ApiKey.create_for_user(self.request.user, name=name) self.request.session[NEW_API_KEY_SESSION_KEY] = { "name": api_key.name, "key_prefix": api_key.key_prefix, "raw_key": raw_key, } messages.success(self.request, "API Key 已生成") return super().form_valid(form) class ApiKeyDeleteView(LoginRequiredMixin, View): def post(self, request, pk): api_key = get_object_or_404(ApiKey, pk=pk, user=request.user) if api_key.status != ApiKey.Status.REVOKED: api_key.status = ApiKey.Status.REVOKED api_key.save(update_fields=("status", "updated_at")) messages.success(request, "API Key 已删除") return redirect("portal-apikeys") class ModelCatalogView(LoginRequiredMixin, TemplateView): template_name = "portal/models.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["models"] = get_public_model_catalog() return context class RechargePageView(LoginRequiredMixin, FormView): template_name = "portal/recharge.html" form_class = RechargeCreateForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) order_no = str(self.request.GET.get("order_no") or "").strip() current_order = None if order_no: current_order = RechargeOrder.objects.filter( user=self.request.user, order_no=order_no, ).first() context["balance"] = get_balance_snapshot(self.request.user) context.update(get_portal_account_summary(self.request.user)) current_payment_mode = payment_callback_mode() context["current_order"] = current_order context["payment_callback_mode"] = current_payment_mode context["is_mock_payment_mode"] = current_payment_mode == "mock" context["recent_recharge_orders"] = get_recharge_orders_for_user( self.request.user )[:5] return context def form_valid(self, form): try: order = create_recharge_order( user=self.request.user, amount=form.cleaned_data["amount"], pay_method=form.cleaned_data["pay_method"], ) except NoExchangeRateError: form.add_error(None, "未配置当前币种汇率,暂时无法充值。") return self.form_invalid(form) except RechargeOrderCreateError: form.add_error(None, "充值下单参数错误,请检查金额和支付方式。") return self.form_invalid(form) except PaymentOrderCreateError: form.add_error(None, "支付下单失败,请稍后重试。") return self.form_invalid(form) messages.success(self.request, "充值订单已创建,请扫码支付。") recharge_url = reverse("portal-recharge") return redirect(f"{recharge_url}?order_no={order.order_no}") class RechargeRecordListView(LoginRequiredMixin, TemplateView): template_name = "portal/recharge_records.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) orders = get_recharge_orders_for_user(self.request.user) pagination = paginate_records(self.request, orders) context.update(get_portal_account_summary(self.request.user)) context.update(pagination) context["recharge_orders"] = pagination["page_obj"].object_list return context class UsageRecordListView(LoginRequiredMixin, TemplateView): template_name = "portal/usage_records.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) entries = get_usage_ledger_entries_for_user(self.request.user) pagination = paginate_records(self.request, entries) context.update(get_portal_account_summary(self.request.user)) context.update(pagination) context["usage_entries"] = pagination["page_obj"].object_list return context