feat: add software subscription orders

This commit is contained in:
QiuSW
2026-07-21 11:52:49 +08:00
parent 341864dc70
commit bc9d1aa0f7
25 changed files with 1179 additions and 47 deletions
+1
View File
@@ -17,6 +17,7 @@
{% if user.is_authenticated %}
<a class="btn btn-sm {% if current_url == 'portal-dashboard' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-dashboard' %} aria-current="page"{% endif %} href="{% url 'portal-dashboard' %}">控制台</a>
<a class="btn btn-sm {% if current_url == 'portal-recharge' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-recharge' %} aria-current="page"{% endif %} href="{% url 'portal-recharge' %}">充值</a>
<a class="btn btn-sm {% if current_url == 'portal-subscription' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-subscription' %} aria-current="page"{% endif %} href="{% url 'portal-subscription' %}">软件订阅</a>
<a class="btn btn-sm {% if current_url == 'portal-apikeys' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-apikeys' %} aria-current="page"{% endif %} href="{% url 'portal-apikeys' %}">API Key</a>
<a class="btn btn-sm {% if current_url == 'portal-models' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-models' %} aria-current="page"{% endif %} href="{% url 'portal-models' %}">可用模型</a>
<a class="btn btn-sm {% if current_url == 'portal-recharge-records' %}btn-primary{% else %}btn-outline-secondary{% endif %}"{% if current_url == 'portal-recharge-records' %} aria-current="page"{% endif %} href="{% url 'portal-recharge-records' %}">充值记录</a>
@@ -0,0 +1,109 @@
{% extends "portal/base.html" %}
{% load static %}
{% block title %}软件订阅 - 虾皮圈{% endblock %}
{% block content %}
<div class="d-flex flex-column gap-4">
<div>
<h1 class="h3 mb-1">软件订阅</h1>
<div class="text-secondary">虾皮圈优化助手</div>
</div>
<section class="cmhub-surface">
<h2 class="h5 mb-3">当前授权</h2>
{% if entitlement %}
<dl class="row mb-0">
<dt class="col-sm-3">套餐</dt><dd class="col-sm-9">{{ entitlement.plan_name }}</dd>
<dt class="col-sm-3">状态</dt><dd class="col-sm-9">{{ entitlement.status }}</dd>
<dt class="col-sm-3">到期时间</dt><dd class="col-sm-9">{{ entitlement.expires_at|date:"Y-m-d H:i" }}</dd>
<dt class="col-sm-3">宽限截止</dt><dd class="col-sm-9">{{ entitlement.grace_expires_at|date:"Y-m-d H:i" }}</dd>
</dl>
{% else %}
<div class="text-secondary">暂无有效授权记录</div>
{% endif %}
</section>
<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-6">
<label class="form-label" for="{{ form.plan.id_for_label }}">{{ form.plan.label }}</label>
{{ form.plan }}
{% if form.plan.errors %}<div class="text-danger small mt-1">{{ form.plan.errors|striptags }}</div>{% endif %}
</div>
<div class="col-md-3">
<div class="form-label">{{ form.pay_method.label }}</div>
{% 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>
<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-software-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-software-order-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="software-order-status">{{ current_order.status }}</span>
</div>
{% if is_mock_payment_mode %}<div class="alert alert-warning">当前为支付测试模式,二维码不能用于真实付款。</div>{% endif %}
<div class="row g-4 align-items-start">
<div class="col-md-5"><canvas id="software-order-qr" class="border rounded bg-white p-2" width="240" height="240"></canvas></div>
<div class="col-md-7">
<dl class="row mb-0">
<dt class="col-sm-4">套餐</dt><dd class="col-sm-8">{{ current_order.plan_name }}</dd>
<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.expires_at|date:"Y-m-d H:i"|default:"-" }}</dd>
<dt class="col-sm-4">权益发放时间</dt><dd class="col-sm-8" id="software-order-fulfilled">{{ current_order.fulfilled_at|date:"Y-m-d H:i"|default:"-" }}</dd>
</dl>
<div class="alert alert-info mt-3 mb-0" id="software-order-hint" aria-live="polite">正在等待支付结果。</div>
</div>
</div>
</section>
{% endif %}
<section class="cmhub-surface">
<h2 class="h5 mb-3">最近订单</h2>
{% if recent_orders %}
<div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>订单号</th><th>套餐</th><th>金额</th><th>状态</th><th>创建时间</th></tr></thead><tbody>
{% for order in recent_orders %}<tr><td><code>{{ order.order_no }}</code></td><td>{{ order.plan_name }}</td><td>{{ order.amount_money|floatformat:2 }} {{ order.currency }}</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="{% static 'portal/vendor/qrcode/qrcode.js' %}"></script>
<script>
(function () {
const panel = document.querySelector("[data-software-order]");
if (!panel) return;
const canvas = document.getElementById("software-order-qr");
const statusBadge = document.getElementById("software-order-status");
const fulfilled = document.getElementById("software-order-fulfilled");
const hint = document.getElementById("software-order-hint");
if (panel.dataset.codeUrl && window.QRCode && typeof window.QRCode.toCanvas === "function") {
window.QRCode.toCanvas(canvas, panel.dataset.codeUrl, { width: 240, margin: 1 });
}
function poll() {
fetch(panel.dataset.statusUrl + "?order_no=" + encodeURIComponent(panel.dataset.orderNo), { credentials: "same-origin", headers: { Accept: "application/json" } })
.then(function (response) { return response.json().then(function (data) { if (!response.ok) throw new Error(); return data; }); })
.then(function (data) {
statusBadge.textContent = data.status;
statusBadge.className = data.status === "paid" ? "badge text-bg-success" : "badge text-bg-secondary";
if (data.fulfilled_at) fulfilled.textContent = new Date(data.fulfilled_at).toLocaleString();
if (data.status === "paid") { hint.textContent = "订阅权益已发放,页面即将刷新。"; window.setTimeout(function () { window.location.reload(); }, 1200); return; }
hint.textContent = data.status === "pending" ? "正在等待支付结果。" : "订单当前状态为 " + data.status + "。";
if (data.status === "pending") window.setTimeout(poll, 1000);
})
.catch(function () { hint.textContent = "状态查询失败,稍后自动重试。"; window.setTimeout(poll, 3000); });
}
if (panel.dataset.status === "pending") window.setTimeout(poll, 1000);
}());
</script>
{% endblock %}