feat: add portal recharge page
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from apps.billing.models import RechargeOrder
|
||||
|
||||
|
||||
class ApiKeyCreateForm(forms.Form):
|
||||
@@ -14,3 +19,37 @@ class ApiKeyCreateForm(forms.Form):
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RechargeCreateForm(forms.Form):
|
||||
amount = forms.DecimalField(
|
||||
label="充值金额",
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
min_value=Decimal("0.01"),
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"inputmode": "decimal",
|
||||
"min": "0.01",
|
||||
"step": "0.01",
|
||||
"placeholder": "例如:100.00",
|
||||
}
|
||||
),
|
||||
)
|
||||
pay_method = forms.ChoiceField(
|
||||
label="支付方式",
|
||||
choices=(
|
||||
(RechargeOrder.PayMethod.WEIXIN, "微信"),
|
||||
(RechargeOrder.PayMethod.ALIPAY, "支付宝"),
|
||||
),
|
||||
initial=RechargeOrder.PayMethod.WEIXIN,
|
||||
widget=forms.RadioSelect(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
|
||||
def clean_amount(self):
|
||||
amount = self.cleaned_data["amount"]
|
||||
max_amount = Decimal(str(settings.RECHARGE_MAX_AMOUNT_CNY))
|
||||
if amount > max_amount:
|
||||
raise forms.ValidationError(f"单笔充值金额不能超过 {max_amount:.2f} CNY")
|
||||
return amount
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
<div class="ms-auto d-flex gap-2 flex-wrap justify-content-end">
|
||||
{% if user.is_authenticated %}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-dashboard' %}">控制台</a>
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'portal-recharge' %}">充值</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-apikeys' %}">API Key</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-recharge-records' %}">充值记录</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-usage-records' %}">消费记录</a>
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
<div class="metric d-flex flex-column gap-3">
|
||||
<div>
|
||||
<div class="text-secondary small">充值</div>
|
||||
<div class="h5 mb-0">充值记录</div>
|
||||
<div class="h5 mb-0">扫码充值</div>
|
||||
</div>
|
||||
<a class="btn btn-primary align-self-start" href="{% url 'portal-recharge-records' %}">查看</a>
|
||||
<a class="btn btn-primary align-self-start" href="{% url 'portal-recharge' %}">充值</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% block title %}充值 - cmhub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex flex-column gap-4">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">充值</h1>
|
||||
<div class="text-secondary">{{ user.email }}</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="{% url 'portal-recharge-records' %}">充值记录</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="metric">
|
||||
<div class="text-secondary small">当前点数</div>
|
||||
<div class="display-6 fw-semibold">{{ balance.points_balance }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="metric">
|
||||
<div class="text-secondary small">充值总额</div>
|
||||
<div class="display-6 fw-semibold">{{ recharge_total_amount|floatformat:2 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="metric">
|
||||
<div class="text-secondary small">入账点数</div>
|
||||
<div class="display-6 fw-semibold">{{ recharge_points_total }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="cmhub-surface">
|
||||
<h2 class="h5 mb-3">发起充值</h2>
|
||||
<form method="post" class="row g-3 align-items-end" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-danger mb-0">{{ form.non_field_errors|striptags }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-5">
|
||||
<label class="form-label" for="{{ form.amount.id_for_label }}">{{ form.amount.label }}</label>
|
||||
{{ form.amount }}
|
||||
{% if form.amount.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.amount.errors|striptags }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-label">{{ form.pay_method.label }}</div>
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
{% for radio in form.pay_method %}
|
||||
<div class="form-check">
|
||||
{{ radio.tag }}
|
||||
<label class="form-check-label" for="{{ radio.id_for_label }}">{{ radio.choice_label }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if form.pay_method.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.pay_method.errors|striptags }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button class="btn btn-primary w-100" type="submit">创建订单</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if current_order %}
|
||||
<section
|
||||
class="cmhub-surface"
|
||||
data-recharge-order
|
||||
data-order-no="{{ current_order.order_no }}"
|
||||
data-status="{{ current_order.status }}"
|
||||
data-code-url="{{ current_order.code_url }}"
|
||||
data-status-url="{% url 'api-recharge-status' %}"
|
||||
>
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-3">
|
||||
<div>
|
||||
<h2 class="h5 mb-1">待支付订单</h2>
|
||||
<div class="text-secondary small">订单号:<code>{{ current_order.order_no }}</code></div>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary" id="recharge-status-badge">{{ current_order.status }}</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-md-5">
|
||||
<canvas id="recharge-qr" class="border rounded bg-white p-2" width="240" height="240"></canvas>
|
||||
<div id="recharge-qr-fallback" class="d-none mt-3">
|
||||
<label class="form-label" for="recharge-code-url">支付票据</label>
|
||||
<textarea id="recharge-code-url" class="form-control font-monospace key-value" rows="4" readonly>{{ current_order.code_url }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">金额</dt>
|
||||
<dd class="col-sm-8">{{ current_order.amount_money|floatformat:2 }} {{ current_order.currency }}</dd>
|
||||
<dt class="col-sm-4">预计到账</dt>
|
||||
<dd class="col-sm-8">{{ current_order.points_granted }} 点</dd>
|
||||
<dt class="col-sm-4">支付方式</dt>
|
||||
<dd class="col-sm-8">{{ current_order.pay_method }}</dd>
|
||||
<dt class="col-sm-4">创建时间</dt>
|
||||
<dd class="col-sm-8">{{ current_order.created_at|date:"Y-m-d H:i" }}</dd>
|
||||
<dt class="col-sm-4">二维码有效期</dt>
|
||||
<dd class="col-sm-8">{{ current_order.expires_at|date:"Y-m-d H:i"|default:"-" }}</dd>
|
||||
<dt class="col-sm-4">到账时间</dt>
|
||||
<dd class="col-sm-8" id="recharge-paid-at">{{ current_order.paid_at|date:"Y-m-d H:i"|default:"-" }}</dd>
|
||||
</dl>
|
||||
<div class="alert alert-info mt-3 mb-0" id="recharge-poll-hint" aria-live="polite">
|
||||
{% if current_order.status == "pending" %}
|
||||
正在等待支付结果,页面会自动轮询订单状态。
|
||||
{% elif current_order.status == "paid" %}
|
||||
订单已到账,余额已刷新。
|
||||
{% else %}
|
||||
订单当前状态为 {{ current_order.status }}。
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="cmhub-surface">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h5 mb-0">最近充值</h2>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'portal-recharge-records' %}">全部</a>
|
||||
</div>
|
||||
{% if recent_recharge_orders %}
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">订单号</th>
|
||||
<th scope="col">金额</th>
|
||||
<th scope="col">点数</th>
|
||||
<th scope="col">状态</th>
|
||||
<th scope="col">创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in recent_recharge_orders %}
|
||||
<tr>
|
||||
<td><code>{{ order.order_no }}</code></td>
|
||||
<td>{{ order.amount_money|floatformat:2 }} {{ order.currency }}</td>
|
||||
<td>{{ order.points_granted }}</td>
|
||||
<td>{{ order.status }}</td>
|
||||
<td>{{ order.created_at|date:"Y-m-d H:i" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-secondary">暂无充值记录</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const panel = document.querySelector("[data-recharge-order]");
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orderNo = panel.dataset.orderNo;
|
||||
const codeUrl = panel.dataset.codeUrl;
|
||||
const statusUrl = panel.dataset.statusUrl;
|
||||
const statusBadge = document.getElementById("recharge-status-badge");
|
||||
const paidAt = document.getElementById("recharge-paid-at");
|
||||
const pollHint = document.getElementById("recharge-poll-hint");
|
||||
const fallback = document.getElementById("recharge-qr-fallback");
|
||||
const canvas = document.getElementById("recharge-qr");
|
||||
const labels = {
|
||||
pending: "pending",
|
||||
paid: "paid",
|
||||
failed: "failed",
|
||||
expired: "expired"
|
||||
};
|
||||
|
||||
function showQrFallback() {
|
||||
if (fallback) {
|
||||
fallback.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
if (codeUrl && window.QRCode && typeof window.QRCode.toCanvas === "function") {
|
||||
window.QRCode.toCanvas(canvas, codeUrl, { width: 240, margin: 1 }, function (error) {
|
||||
if (error) {
|
||||
showQrFallback();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showQrFallback();
|
||||
}
|
||||
|
||||
function setStatus(data) {
|
||||
const status = data.status || "pending";
|
||||
statusBadge.textContent = labels[status] || status;
|
||||
statusBadge.className = status === "paid" ? "badge text-bg-success" : "badge text-bg-secondary";
|
||||
paidAt.textContent = data.paid_at ? new Date(data.paid_at).toLocaleString() : "-";
|
||||
if (status === "paid") {
|
||||
pollHint.textContent = "订单已到账,正在刷新余额。";
|
||||
} else if (status === "pending" && data.is_expired) {
|
||||
pollHint.textContent = "二维码已过期,可重新发起一笔充值;若已支付,到账仍以回调为准。";
|
||||
} else if (status === "pending") {
|
||||
pollHint.textContent = "正在等待支付结果,页面会自动轮询订单状态。";
|
||||
} else {
|
||||
pollHint.textContent = "订单当前状态为 " + status + "。";
|
||||
}
|
||||
}
|
||||
|
||||
function pollStatus() {
|
||||
fetch(statusUrl + "?order_no=" + encodeURIComponent(orderNo), {
|
||||
method: "GET",
|
||||
credentials: "same-origin",
|
||||
headers: { Accept: "application/json" }
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.json().then(function (data) {
|
||||
if (!response.ok) {
|
||||
throw new Error((data.error && data.error.message) || "status request failed");
|
||||
}
|
||||
return data;
|
||||
});
|
||||
})
|
||||
.then(function (data) {
|
||||
setStatus(data);
|
||||
if (data.status === "paid") {
|
||||
window.setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 1200);
|
||||
return;
|
||||
}
|
||||
if (data.status === "pending") {
|
||||
window.setTimeout(pollStatus, 1000);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
pollHint.textContent = "状态查询失败,稍后自动重试。";
|
||||
window.setTimeout(pollStatus, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
if (panel.dataset.status === "pending") {
|
||||
window.setTimeout(pollStatus, 1000);
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
{% endblock %}
|
||||
+113
-1
@@ -9,7 +9,7 @@ from django.test import Client, TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.billing.models import CallRecord, PointsLedger, RechargeOrder
|
||||
from apps.billing.models import CallRecord, ExchangeRate, PointsLedger, RechargeOrder
|
||||
from apps.users.models import ApiKey, UserWallet
|
||||
|
||||
|
||||
@@ -434,3 +434,115 @@ class PortalAccountFlowTests(TestCase):
|
||||
self.assertContains(response, "15")
|
||||
self.assertNotContains(response, "other-alias")
|
||||
self.assertNotContains(response, other_key.key_prefix)
|
||||
|
||||
def test_recharge_page_requires_login_and_shows_form(self):
|
||||
user = self.create_verified_user()
|
||||
UserWallet.objects.create(user=user, points_balance=25)
|
||||
|
||||
anonymous_response = self.client.get("/recharge")
|
||||
self.assertEqual(anonymous_response.status_code, 302)
|
||||
self.assertTrue(anonymous_response["Location"].startswith("/login?next="))
|
||||
|
||||
self.client.force_login(user)
|
||||
response = self.client.get("/recharge")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context["balance"].points_balance, 25)
|
||||
self.assertContains(response, "充值金额")
|
||||
self.assertContains(response, "创建订单")
|
||||
self.assertContains(response, "qrcode@1.5.4")
|
||||
self.assertContains(response, "QRCode.toCanvas")
|
||||
|
||||
def test_recharge_page_post_creates_pending_order_without_crediting_wallet_or_ledger(self):
|
||||
user = self.create_verified_user()
|
||||
UserWallet.objects.create(user=user, points_balance=5)
|
||||
ExchangeRate.objects.create(
|
||||
currency="CNY",
|
||||
points_per_unit=Decimal("10.0000"),
|
||||
effective_from=timezone.now(),
|
||||
)
|
||||
self.client.force_login(user)
|
||||
|
||||
response = self.client.post(
|
||||
"/recharge",
|
||||
{"amount": "20.00", "pay_method": RechargeOrder.PayMethod.WEIXIN},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response["Location"].startswith("/recharge?order_no="))
|
||||
order = RechargeOrder.objects.get(user=user)
|
||||
self.assertEqual(order.amount_money, Decimal("20.00"))
|
||||
self.assertEqual(order.exchange_rate, Decimal("10.0000"))
|
||||
self.assertEqual(order.points_granted, 200)
|
||||
self.assertEqual(order.status, RechargeOrder.Status.PENDING)
|
||||
self.assertTrue(order.code_url.startswith("weixin://wxpay/cmhub-mock"))
|
||||
self.assertIsNotNone(order.expires_at)
|
||||
wallet = UserWallet.objects.get(user=user)
|
||||
self.assertEqual(wallet.points_balance, 5)
|
||||
self.assertFalse(
|
||||
PointsLedger.objects.filter(
|
||||
user=user,
|
||||
ref_order_id=order.id,
|
||||
).exists()
|
||||
)
|
||||
|
||||
follow_response = self.client.get(response["Location"])
|
||||
|
||||
self.assertEqual(follow_response.status_code, 200)
|
||||
self.assertEqual(follow_response.context["current_order"], order)
|
||||
self.assertContains(follow_response, order.order_no)
|
||||
self.assertContains(follow_response, "weixin://wxpay/cmhub-mock")
|
||||
self.assertContains(follow_response, 'data-status-url="/api/v1/recharge/status"')
|
||||
self.assertContains(follow_response, "data-recharge-order")
|
||||
|
||||
def test_recharge_page_current_order_only_shows_current_user_order(self):
|
||||
user = self.create_verified_user()
|
||||
other_user = self.create_verified_user()
|
||||
UserWallet.objects.create(user=user, points_balance=0)
|
||||
other_order = self.create_recharge_order(
|
||||
other_user,
|
||||
order_no="R-OTHER-RECHARGE-PAGE-504",
|
||||
amount="50.00",
|
||||
points=500,
|
||||
status=RechargeOrder.Status.PENDING,
|
||||
)
|
||||
self.client.force_login(user)
|
||||
|
||||
response = self.client.get(f"/recharge?order_no={other_order.order_no}")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNone(response.context["current_order"])
|
||||
self.assertNotContains(response, other_order.order_no)
|
||||
|
||||
@override_settings(RECHARGE_MAX_AMOUNT_CNY="100.00")
|
||||
def test_recharge_page_rejects_amount_above_configured_maximum(self):
|
||||
user = self.create_verified_user()
|
||||
UserWallet.objects.create(user=user, points_balance=0)
|
||||
ExchangeRate.objects.create(
|
||||
currency="CNY",
|
||||
points_per_unit=Decimal("10.0000"),
|
||||
effective_from=timezone.now(),
|
||||
)
|
||||
self.client.force_login(user)
|
||||
|
||||
response = self.client.post(
|
||||
"/recharge",
|
||||
{"amount": "100.01", "pay_method": RechargeOrder.PayMethod.ALIPAY},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "单笔充值金额不能超过 100.00 CNY")
|
||||
self.assertFalse(RechargeOrder.objects.filter(user=user).exists())
|
||||
|
||||
def test_recharge_page_post_is_csrf_protected(self):
|
||||
user = self.create_verified_user()
|
||||
csrf_client = Client(enforce_csrf_checks=True)
|
||||
csrf_client.force_login(user)
|
||||
|
||||
response = csrf_client.post(
|
||||
"/recharge",
|
||||
{"amount": "20.00", "pay_method": RechargeOrder.PayMethod.WEIXIN},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertFalse(RechargeOrder.objects.filter(user=user).exists())
|
||||
|
||||
@@ -6,6 +6,7 @@ from .views import (
|
||||
ApiKeyDeleteView,
|
||||
ApiKeyListCreateView,
|
||||
DashboardView,
|
||||
RechargePageView,
|
||||
RechargeRecordListView,
|
||||
UsageRecordListView,
|
||||
)
|
||||
@@ -18,6 +19,7 @@ urlpatterns = [
|
||||
path("dashboard", DashboardView.as_view(), name="portal-dashboard"),
|
||||
path("apikeys", ApiKeyListCreateView.as_view(), name="portal-apikeys"),
|
||||
path("apikeys/<int:pk>/delete", ApiKeyDeleteView.as_view(), name="portal-apikey-delete"),
|
||||
path("recharge", RechargePageView.as_view(), name="portal-recharge"),
|
||||
path("records/recharge", RechargeRecordListView.as_view(), name="portal-recharge-records"),
|
||||
path("records/usage", UsageRecordListView.as_view(), name="portal-usage-records"),
|
||||
]
|
||||
|
||||
+52
-3
@@ -2,15 +2,21 @@ from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Sum
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
||||
from apps.billing.payment_gateways import PaymentOrderCreateError
|
||||
from apps.billing.pricing import NoExchangeRateError
|
||||
from apps.billing.models import PointsLedger, RechargeOrder
|
||||
from apps.billing.services import get_balance_snapshot
|
||||
from apps.billing.services import (
|
||||
RechargeOrderCreateError,
|
||||
create_recharge_order,
|
||||
get_balance_snapshot,
|
||||
)
|
||||
from apps.users.models import ApiKey
|
||||
|
||||
from .forms import ApiKeyCreateForm
|
||||
from .forms import ApiKeyCreateForm, RechargeCreateForm
|
||||
|
||||
|
||||
NEW_API_KEY_SESSION_KEY = "portal_new_api_key"
|
||||
@@ -120,6 +126,49 @@ class ApiKeyDeleteView(LoginRequiredMixin, View):
|
||||
return redirect("portal-apikeys")
|
||||
|
||||
|
||||
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))
|
||||
context["current_order"] = current_order
|
||||
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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user