Files
bili-auto-comment/buy.js
T

321 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 当前商品选择页:选套餐 → 选支付方式 → 下单 → 支付 → 自动激活本机 */
const $ = (id) => document.getElementById(id);
let packages = [];
let selected = null;
let orderNo = null;
let pollTimer = null;
let lastPayType = null; // 记住当前支付方式,「重新下单」时直接复用
let payHelpShown = false;
let helpTipStage = 0; // 1=30秒提示 2=120秒提示;同阶段不重复覆盖刚点出来的反馈
const PENDING_KEY = 'biliBuyPendingOrder';
/** 订单号落本地:关掉页面再回来也能接着查卡,不会「付了钱拿不到码」 */
function savePending() {
try {
localStorage.setItem(PENDING_KEY, JSON.stringify({
orderNo, payType: lastPayType,
cardTypeId: selected ? selected.id : null, at: Date.now()
}));
} catch (e) { /* 隐私模式下不可用则忽略 */ }
}
function clearPending() {
try { localStorage.removeItem(PENDING_KEY); } catch (e) {}
}
function readPending(maxAgeMs) {
try {
const d = JSON.parse(localStorage.getItem(PENDING_KEY) || 'null');
if (!d || !d.orderNo) return null;
if (!d.at || Date.now() - d.at > maxAgeMs) { clearPending(); return null; }
return d;
} catch (e) { return null; }
}
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
function durationText(days) {
const n = Number(days);
if (!n) return '限时体验';
if (n >= 999999) return '永久有效';
return n + ' 天';
}
async function loadPackages() {
$('package-list').innerHTML = '<div class="loading">正在加载当前商品…</div>';
try {
const res = await chrome.runtime.sendMessage({ action: 'getCardTypes' });
if (!res || !res.ok) throw new Error((res && res.message) || '加载失败');
packages = (res.data || []).filter((p) => Number(p.price) > 0);
if (!packages.length) {
$('package-list').innerHTML = '<div class="empty">当前暂无在售商品,请稍后再试</div>';
return;
}
renderPackages();
} catch (e) {
$('package-list').innerHTML = '<div class="empty">商品加载失败:' + esc(e.message) + '</div>';
}
}
function renderPackages() {
$('package-list').innerHTML = packages.map((p) => `
<div class="package" data-id="${esc(p.id)}">
<div class="pkg-name">${esc(p.name)}</div>
<div class="pkg-price">¥${esc(p.price)}</div>
<div class="pkg-dur">${esc(durationText(p.duration_days))}</div>
<button class="pkg-btn" data-id="${esc(p.id)}">选择此商品</button>
</div>`).join('');
document.querySelectorAll('.pkg-btn').forEach((btn) => {
btn.addEventListener('click', () => selectPackage(btn.dataset.id));
});
}
function selectPackage(id) {
selected = packages.find((p) => String(p.id) === String(id));
if (!selected) return;
document.querySelectorAll('.package').forEach((el) => {
el.classList.toggle('active', el.dataset.id === String(id));
});
$('chosen-package').innerHTML = `已选商品:<b>${esc(selected.name)}</b> 价格 <b>¥${esc(selected.price)}</b> ${esc(durationText(selected.duration_days))}`;
$('step-pay').hidden = false;
$('step-pay').scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async function createOrder(payType) {
if (!selected) return;
lastPayType = payType;
$('order-panel').hidden = false;
$('order-panel').scrollIntoView({ behavior: 'smooth', block: 'center' });
hidePayHelp();
const status = $('order-status');
status.className = 'order-status pending';
status.textContent = '⏳ 正在创建订单…';
try {
const res = await chrome.runtime.sendMessage({
action: 'authCreateOrder',
payType,
cardTypeId: selected.id
});
if (!res || !res.ok) throw new Error((res && res.message) || '创建订单失败');
orderNo = res.order.orderNo;
savePending();
status.textContent = `订单 ${orderNo}(¥${res.order.amount})已创建,正在打开支付页面…`;
if (res.payUrl) window.open(res.payUrl, '_blank');
startPolling();
} catch (e) {
status.className = 'order-status error';
status.textContent = '✗ ' + e.message;
}
}
let attempts = 0;
/** 单次订单查询:5 秒轮询与「立即刷新核对」按钮共用同一段逻辑 */
async function queryOrderOnce() {
attempts++;
if (attempts > 240) {
clearInterval(pollTimer);
pollTimer = null;
const status = $('order-status');
status.className = 'order-status error';
status.textContent = `✗ 等待支付超时。订单号 ${orderNo},如已付款请把订单号发给官网客服处理。`;
return;
}
try {
const res = await chrome.runtime.sendMessage({ action: 'authQueryOrder', orderNo });
if (!res || !res.ok) return;
// 只要拿到卡密就先展示出来,不等激活结果——自动激活失败时用户手里必须有码
if (res.cardKey) showCardBox(res.cardKey, !!res.activated);
if (res.activated) {
clearInterval(pollTimer);
pollTimer = null;
clearPending();
showActivated(res);
} else if (res.cardKey) {
clearInterval(pollTimer);
pollTimer = null;
hidePayHelp();
clearPending();
const status = $('order-status');
status.className = 'order-status error';
status.innerHTML = '✗ <b>已支付并已发卡</b>,但自动激活未成功(' +
esc(res.message || '本机绑定可能被占用') + ')。请用上方卡密手动激活:回到插件侧边栏 → 卡密输入框粘贴 → 点「粘贴卡密并激活」。<br>' +
'订单号:<b>' + esc(orderNo || '') + '</b>';
} else if (res.paid) {
// 已付款但服务端还没把卡发出来(最常见原因是卡池库存不足)
const waited = attempts * 5;
const status = $('order-status');
status.className = 'order-status pending';
status.textContent = '⏳ 已支付,正在为你发卡…(订单 ' + orderNo + ',已等待 ' + waited + ' 秒)';
if (waited >= 90) {
status.className = 'order-status error';
status.innerHTML = '⏳ <b>已支付</b>,但发卡耗时较长(已 ' + waited + ' 秒)。订单 <b>' + esc(orderNo) +
'</b> 付款状态为已支付,<b>权益不会丢失</b>:继续等会自动发放,或凭此订单号联系客服补发。';
}
} else {
const waited = attempts * 5;
const status = $('order-status');
status.className = 'order-status pending';
status.textContent = '⏳ 等待支付中…(订单 ' + orderNo + ',已等待 ' + waited + ' 秒)';
// 30 秒还没动静就主动给出口(付款通知丢失 / 扫了过期码 / 实际没扣款)
if (waited >= 30) showPayHelp(waited);
}
} catch (e) {
/* 单次查询失败不中断轮询 */
}
}
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
attempts = 0;
hidePayHelp();
queryOrderOnce(); // 先立刻查一次,不干等 5 秒
pollTimer = setInterval(queryOrderOnce, 5000);
}
/** 付款迟迟没动静时给出路,别让用户对着「等待支付中」干等 */
function showPayHelp(waited) {
const box = $('pay-help');
if (!box) return;
box.hidden = false;
payHelpShown = true;
const tip = $('ph-tip');
if (!tip) return;
const stage = waited >= 120 ? 2 : 1;
if (tip.textContent && stage <= helpTipStage) return; // 不覆盖用户刚点「刷新」得到的反馈
helpTipStage = stage;
if (stage === 2) {
tip.textContent = '已等待 ' + waited + ' 秒仍未确认支付。若你确已完成付款:请勿重复支付,留好订单号 ' +
(orderNo || '') + ' 与微信/支付宝交易号;若二维码是旧的/已过期,点「重新下单」换一个新码再付。';
} else {
tip.textContent = '正在等待付款通知…已完成付款的话,点「立即刷新核对」可以马上重查一次。';
}
}
function hidePayHelp() {
const box = $('pay-help');
if (box) box.hidden = true;
payHelpShown = false;
helpTipStage = 0;
const tip = $('ph-tip');
if (tip) tip.textContent = '';
}
/** 打开购买页时恢复上次未完成的订单:关掉页面也不会「付了钱拿不到码」 */
function resumePending() {
const d = readPending(30 * 60 * 1000);
if (!d) return;
orderNo = d.orderNo;
if (d.payType) lastPayType = d.payType;
if (d.cardTypeId && !selected) {
const p = packages.find((x) => String(x.id) === String(d.cardTypeId));
if (p) {
selected = p;
document.querySelectorAll('.package').forEach((el) => {
el.classList.toggle('active', el.dataset.id === String(d.cardTypeId));
});
$('chosen-package').innerHTML = `已选商品:<b>${esc(p.name)}</b> 价格 <b>¥${esc(p.price)}</b> ${esc(durationText(p.duration_days))}`;
$('step-pay').hidden = false;
}
}
$('order-panel').hidden = false;
const st = $('order-status');
st.className = 'order-status pending';
st.textContent = '⏳ 已恢复上次未完成的订单 ' + orderNo + ',正在查询支付结果…';
startPolling();
}
function wirePayHelp() {
const rf = $('ph-refresh'), rn = $('ph-renew');
if (rf) rf.addEventListener('click', async () => {
const tip = $('ph-tip');
if (tip) tip.textContent = '正在向授权系统核对…';
await queryOrderOnce();
const box = $('pay-help');
if (!box || box.hidden || !tip) return; // 已出卡激活就不必再提示
tip.textContent = '已于 ' + new Date().toLocaleTimeString() +
' 重新核对:仍未确认支付。若你确已完成付款请勿重复支付(通知可能延迟);二维码过期就点「重新下单」。';
});
if (rn) rn.addEventListener('click', () => {
if (!selected) {
const tip = $('ph-tip');
if (tip) tip.textContent = '请先在上方重新选择套餐,再选支付方式生成新订单。';
const sp = $('step-package');
if (sp) sp.scrollIntoView({ behavior: 'smooth', block: 'start' });
return;
}
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
createOrder(lastPayType || 'wxpay');
});
}
/** 支付成功后立刻把卡密展示给用户(含自动激活失败的场景) */
function showCardBox(key, activated) {
const box = $('card-box');
if (!box || !key) return;
if (!box.hidden && $('cb-key-text').textContent === key) return;
$('cb-key-text').textContent = key;
box.hidden = false;
$('cb-copy-tip').textContent = activated ? '已自动激活本机,这是你的卡密,建议留存' : '';
box.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function showActivated(res) {
hidePayHelp();
const status = $('order-status');
const permanent = !!res.isPermanent;
const exp = String(res.expireTime || '').trim();
const plan = String(res.planName || '').trim();
// 绝不默认「永久」:到期时间只在服务端明确给出时才显示
let expireLine;
if (permanent) expireLine = '到期时间:<b>永久</b>';
else if (exp) expireLine = '到期时间:<b>' + esc(exp) + '</b>';
else if (plan) expireLine = '套餐:<b>' + esc(plan) + '</b>(实际到期时间以插件侧边栏「已授权 · 到期 …」为准)';
else expireLine = '到期时间:以插件侧边栏显示为准';
status.className = 'order-status success';
status.innerHTML = `
<div class="ok-title">✓ 支付成功,已自动激活</div>
<div class="ok-line">卡密:<b>${esc(res.cardKey || '')}</b></div>
<div class="ok-line">${expireLine}</div>
<div class="ok-line muted">回到插件侧边栏即可使用全部功能(本机已授权)。</div>`;
}
document.querySelectorAll('.pay-btn').forEach((btn) => {
btn.addEventListener('click', () => createOrder(btn.dataset.pay));
});
// 复制卡密:优先 clipboard API,无权限时降级 execCommand
$('cb-copy') && $('cb-copy').addEventListener('click', async () => {
const key = ($('cb-key-text').textContent || '').trim();
if (!key) return;
let ok = false;
try {
await navigator.clipboard.writeText(key);
ok = true;
} catch (e) { /* 降级 */ }
if (!ok) {
try {
const ta = document.createElement('textarea');
ta.value = key;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
ok = document.execCommand('copy');
document.body.removeChild(ta);
} catch (e2) {
ok = false;
}
}
$('cb-copy-tip').textContent = ok ? '已复制,可直接粘贴到侧边栏激活' : '复制失败,请手动长按选择卡密';
});
wirePayHelp();
loadPackages().then(resumePending);