239 lines
8.6 KiB
Python
239 lines
8.6 KiB
Python
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
|