fix:优化当前的项目
This commit is contained in:
340
app/static/billing.js
Normal file
340
app/static/billing.js
Normal file
@@ -0,0 +1,340 @@
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function setStatus(msg, danger = false) {
|
||||
const el = $("status");
|
||||
if (!el) return;
|
||||
el.style.color = danger ? "#b42318" : "#0f5f3d";
|
||||
el.textContent = msg;
|
||||
}
|
||||
|
||||
function setLoading(button, loading, idleText, loadingText) {
|
||||
if (!button) return;
|
||||
button.disabled = loading;
|
||||
button.textContent = loading ? loadingText : idleText;
|
||||
}
|
||||
|
||||
function importantNotice(message, title = "提示") {
|
||||
if (typeof window.uiAlert === "function") {
|
||||
void window.uiAlert(message, title);
|
||||
return;
|
||||
}
|
||||
setStatus(message, true);
|
||||
}
|
||||
|
||||
function openPayLink(url) {
|
||||
const payUrl = String(url || "").trim();
|
||||
if (!payUrl) return false;
|
||||
const tab = window.open(payUrl, "_blank", "noopener");
|
||||
if (tab) return true;
|
||||
window.location.href = payUrl;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function postJSON(url, body) {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || "请求失败");
|
||||
return data;
|
||||
}
|
||||
|
||||
async function authMe() {
|
||||
const res = await fetch("/api/auth/me");
|
||||
const data = await res.json();
|
||||
if (!data.logged_in) {
|
||||
window.location.href = "/auth?next=/billing";
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function fmtTime(ts) {
|
||||
const n = Number(ts || 0);
|
||||
if (!n) return "-";
|
||||
return new Date(n * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function mapOrderStatus(status) {
|
||||
const s = String(status || "").toLowerCase();
|
||||
if (s === "paid" || s === "success") return "已支付";
|
||||
if (s === "pending") return "待支付";
|
||||
if (s === "failed") return "支付失败";
|
||||
if (s === "cancelled") return "已取消";
|
||||
if (s === "closed") return "已关闭";
|
||||
return status || "-";
|
||||
}
|
||||
|
||||
function mapChannel(channel) {
|
||||
const c = String(channel || "").toLowerCase();
|
||||
if (c === "wechat") return "微信支付";
|
||||
if (c === "alipay") return "支付宝";
|
||||
if (c === "stripe") return "Stripe";
|
||||
return channel || "-";
|
||||
}
|
||||
|
||||
function formatDetail(detail) {
|
||||
if (!detail || typeof detail !== "object") return "-";
|
||||
const rows = [];
|
||||
if (detail.source_chars) rows.push(`原文${detail.source_chars}字`);
|
||||
if (detail.body_chars) rows.push(`正文${detail.body_chars}字`);
|
||||
if (detail.title_chars) rows.push(`标题${detail.title_chars}字`);
|
||||
if (detail.summary_chars) rows.push(`摘要${detail.summary_chars}字`);
|
||||
if (detail.image_count) rows.push(`图片${detail.image_count}张`);
|
||||
if (detail.prompt_tokens) rows.push(`输入Token:${detail.prompt_tokens}`);
|
||||
if (detail.completion_tokens) rows.push(`输出Token:${detail.completion_tokens}`);
|
||||
if (detail.total_tokens) rows.push(`总Token:${detail.total_tokens}`);
|
||||
if (detail.model) rows.push(`模型:${detail.model}`);
|
||||
if (detail.image_model) rows.push(`生图模型:${detail.image_model}`);
|
||||
if (detail.image_price_package_images && detail.image_price_package_cny) {
|
||||
rows.push(`图片计价:${detail.image_price_package_images}张=${Number(detail.image_price_package_cny).toFixed(2)}元`);
|
||||
}
|
||||
if (detail.credits_rule) rows.push(`规则:${detail.credits_rule}`);
|
||||
if (detail.billed_basis === "usage_tokens") rows.push("按真实Token计费");
|
||||
if (detail.billed_basis === "char_estimate") rows.push("按字符估算计费");
|
||||
if (detail.paid_amount_cny) rows.push(`实付¥${Number(detail.paid_amount_cny).toFixed(2)}`);
|
||||
if (detail.external_txn_id) rows.push(`交易号:${detail.external_txn_id}`);
|
||||
if (detail.credit_source && typeof detail.credit_source === "object") {
|
||||
const seat = Number(detail.credit_source.seat || 0);
|
||||
const shared = Number(detail.credit_source.shared || 0);
|
||||
rows.push(`抵扣来源:席位${seat}/共享${shared}`);
|
||||
}
|
||||
return rows.length ? rows.join(" | ") : "-";
|
||||
}
|
||||
|
||||
function renderRechargeRecords(records) {
|
||||
const el = $("rechargeRecords");
|
||||
if (!el) return;
|
||||
const list = Array.isArray(records) ? records : [];
|
||||
if (!list.length) {
|
||||
el.innerHTML = '<p class="muted small">暂无充值记录</p>';
|
||||
return;
|
||||
}
|
||||
const rows = list
|
||||
.map((r) => {
|
||||
const statusText = mapOrderStatus(r.status);
|
||||
const statusClass =
|
||||
statusText === "已支付"
|
||||
? "paid"
|
||||
: statusText === "待支付"
|
||||
? "pending"
|
||||
: statusText === "支付失败"
|
||||
? "failed"
|
||||
: "closed";
|
||||
return `<tr>
|
||||
<td class="mono">${r.order_no || "-"}</td>
|
||||
<td><span class="billing-badge ${statusClass}">${statusText}</span></td>
|
||||
<td>${mapChannel(r.channel)}</td>
|
||||
<td>${Number(r.token_amount || 0)}</td>
|
||||
<td>¥${Number(r.amount_cny || 0).toFixed(2)}</td>
|
||||
<td>${fmtTime(r.created_at)}</td>
|
||||
<td>${r.paid_at ? fmtTime(r.paid_at) : "-"}</td>
|
||||
<td>${
|
||||
statusText === "待支付"
|
||||
? `<button class="primary pay-now-btn" data-order-no="${r.order_no || ""}" type="button">立即支付</button>`
|
||||
: "-"
|
||||
}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
el.innerHTML = `<table class="billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>状态</th>
|
||||
<th>渠道</th>
|
||||
<th>Credits</th>
|
||||
<th>金额</th>
|
||||
<th>创建时间</th>
|
||||
<th>支付时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function renderConsumeRecords(records) {
|
||||
const el = $("consumeRecords");
|
||||
if (!el) return;
|
||||
const list = Array.isArray(records) ? records : [];
|
||||
if (!list.length) {
|
||||
el.innerHTML = '<p class="muted small">暂无消费记录</p>';
|
||||
return;
|
||||
}
|
||||
const kindTextMap = {
|
||||
trial_grant: "试用赠送",
|
||||
paid_recharge: "充值到账",
|
||||
manual_recharge: "手动充值",
|
||||
rewrite: "AI改写",
|
||||
cover_generate: "封面生成",
|
||||
poster_generate: "段落海报",
|
||||
usage: "模型调用",
|
||||
};
|
||||
const rows = list
|
||||
.map((r) => {
|
||||
const kindText = kindTextMap[r.kind] || r.kind || "-";
|
||||
const delta = `${r.direction === "out" ? "-" : "+"}${Number(r.token_change || 0)}`;
|
||||
const ref = `${r.ref_type || "-"} ${r.ref_id || ""}`.trim();
|
||||
const detail = formatDetail(r.detail);
|
||||
return `<tr>
|
||||
<td>${kindText}</td>
|
||||
<td class="${r.direction === "out" ? "out" : "in"}">${delta}</td>
|
||||
<td>${Number(r.balance_after || 0)}</td>
|
||||
<td>${ref}</td>
|
||||
<td>${detail}</td>
|
||||
<td>${fmtTime(r.created_at)}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
el.innerHTML = `<table class="billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>Credits变动</th>
|
||||
<th>余额</th>
|
||||
<th>关联</th>
|
||||
<th>明细</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
async function refreshBilling() {
|
||||
const data = await fetch("/api/billing/overview").then((r) => r.json());
|
||||
if (!data.ok) throw new Error(data.detail || "账单加载失败");
|
||||
renderRechargeRecords(data.recharge_records || []);
|
||||
renderConsumeRecords(data.consume_records || []);
|
||||
}
|
||||
|
||||
const createRechargeOrderBtn = $("createRechargeOrderBtn");
|
||||
const refreshBillingBtn = $("refreshBillingBtn");
|
||||
const logoutBtn = $("logoutBtn");
|
||||
const billingRechargeTokensInput = $("billingRechargeTokens");
|
||||
const billingRechargeAmountInput = $("billingRechargeAmount");
|
||||
|
||||
function packageRate() {
|
||||
const credits = Number((billingRechargeTokensInput && billingRechargeTokensInput.dataset.packageCredits) || "1500");
|
||||
const amount = Number((billingRechargeAmountInput && billingRechargeAmountInput.dataset.packageAmount) || "19.9");
|
||||
return {
|
||||
packageCredits: Number.isFinite(credits) && credits > 0 ? credits : 1500,
|
||||
packageAmount: Number.isFinite(amount) && amount > 0 ? amount : 19.9,
|
||||
};
|
||||
}
|
||||
|
||||
function syncBillingAmount() {
|
||||
if (!billingRechargeTokensInput || !billingRechargeAmountInput) return;
|
||||
const credits = Number((billingRechargeTokensInput.value || "0").trim());
|
||||
if (!Number.isFinite(credits) || credits <= 0) return;
|
||||
const { packageCredits, packageAmount } = packageRate();
|
||||
const amount = (credits / packageCredits) * packageAmount;
|
||||
billingRechargeAmountInput.value = amount.toFixed(2);
|
||||
}
|
||||
|
||||
if (billingRechargeTokensInput) billingRechargeTokensInput.addEventListener("input", syncBillingAmount);
|
||||
if (billingRechargeAmountInput) billingRechargeAmountInput.readOnly = true;
|
||||
|
||||
if (createRechargeOrderBtn) {
|
||||
createRechargeOrderBtn.addEventListener("click", async () => {
|
||||
setLoading(createRechargeOrderBtn, true, "创建充值订单", "创建中...");
|
||||
try {
|
||||
const tokens = Number((($("billingRechargeTokens") && $("billingRechargeTokens").value) || "0").trim());
|
||||
const amount = Number((($("billingRechargeAmount") && $("billingRechargeAmount").value) || "0").trim());
|
||||
if (!Number.isFinite(tokens) || tokens <= 0) {
|
||||
setStatus("请输入正确的充值 Credits 数量", true);
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
setStatus("请输入正确的支付金额", true);
|
||||
return;
|
||||
}
|
||||
const out = await postJSON("/api/pay/wechat/", {
|
||||
tokens: Math.round(tokens),
|
||||
amount_cny: Number(amount.toFixed(2)),
|
||||
channel: "wechat",
|
||||
});
|
||||
if (!out.ok) {
|
||||
setStatus(out.detail || "创建订单失败", true);
|
||||
return;
|
||||
}
|
||||
const orderNo = out.order && out.order.order_no ? out.order.order_no : "";
|
||||
if (out.pay_url) {
|
||||
openPayLink(out.pay_url);
|
||||
} else {
|
||||
importantNotice("订单已创建,但未获取到支付链接,请检查支付网关配置。", "支付链接缺失");
|
||||
}
|
||||
setStatus(`订单已创建:${orderNo}`);
|
||||
await refreshBilling();
|
||||
} catch (e) {
|
||||
setStatus(e.message || "创建订单失败", true);
|
||||
} finally {
|
||||
setLoading(createRechargeOrderBtn, false, "创建充值订单", "创建中...");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rechargeRecordsWrap = $("rechargeRecords");
|
||||
if (rechargeRecordsWrap) {
|
||||
rechargeRecordsWrap.addEventListener("click", async (evt) => {
|
||||
const btn = evt.target && evt.target.closest ? evt.target.closest(".pay-now-btn") : null;
|
||||
if (!btn) return;
|
||||
const orderNo = (btn.getAttribute("data-order-no") || "").trim();
|
||||
if (!orderNo) return;
|
||||
setLoading(btn, true, "立即支付", "拉起中...");
|
||||
try {
|
||||
const out = await postJSON("/api/billing/recharge/pay-now", { order_no: orderNo });
|
||||
if (!out.ok) {
|
||||
setStatus(out.detail || "拉起支付失败", true);
|
||||
await refreshBilling();
|
||||
return;
|
||||
}
|
||||
if (out.pay_url) {
|
||||
openPayLink(out.pay_url);
|
||||
} else {
|
||||
importantNotice("未获取到支付链接,请检查支付网关配置。", "立即支付失败");
|
||||
}
|
||||
setStatus(`订单 ${orderNo} 已拉起支付。`);
|
||||
await refreshBilling();
|
||||
} catch (e) {
|
||||
setStatus(e.message || "拉起支付失败", true);
|
||||
await refreshBilling();
|
||||
} finally {
|
||||
setLoading(btn, false, "立即支付", "拉起中...");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (refreshBillingBtn) {
|
||||
refreshBillingBtn.addEventListener("click", async () => {
|
||||
setLoading(refreshBillingBtn, true, "刷新账单记录", "刷新中...");
|
||||
try {
|
||||
await refreshBilling();
|
||||
setStatus("账单已刷新。");
|
||||
} catch (e) {
|
||||
setStatus(e.message || "账单刷新失败", true);
|
||||
} finally {
|
||||
setLoading(refreshBillingBtn, false, "刷新账单记录", "刷新中...");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await postJSON("/api/auth/logout", {});
|
||||
} finally {
|
||||
window.location.href = "/auth?next=/";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const me = await authMe();
|
||||
if (!me) return;
|
||||
syncBillingAmount();
|
||||
await refreshBilling();
|
||||
})();
|
||||
Reference in New Issue
Block a user