/** * 支付页面 JavaScript * 支付页面:半自动 + 第三方自动绑卡 + 全自动绑卡任务管理 + 用户完成后自动验订阅 */ const COUNTRY_CURRENCY_MAP = { US: "USD", GB: "GBP", CA: "CAD", AU: "AUD", SG: "SGD", HK: "HKD", JP: "JPY", TR: "TRY", IN: "INR", BR: "BRL", MX: "MXN", DE: "EUR", FR: "EUR", IT: "EUR", ES: "EUR", EU: "EUR", }; const BILLING_STORAGE_KEY = "payment.billing_profile_non_sensitive"; const BILLING_TEMPLATE_STORAGE_KEY = "payment.billing_templates_v1"; const THIRD_PARTY_BIND_URL_STORAGE_KEY = "payment.third_party_bind_api_url"; const BIND_MODE_STORAGE_KEY = "payment.bind_mode"; const THIRD_PARTY_BIND_DEFAULT_URL = "https://twilight-river-f148.482091502.workers.dev/"; const BILLING_TEMPLATE_MAX = 200; const BILLING_COUNTRY_CURRENCY_MAP = { US: "USD", GB: "GBP", CA: "CAD", AU: "AUD", SG: "SGD", HK: "HKD", JP: "JPY", DE: "EUR", FR: "EUR", IT: "EUR", ES: "EUR", }; const COUNTRY_ALIAS_MAP = { us: { code: "US", currency: "USD" }, usa: { code: "US", currency: "USD" }, "united states": { code: "US", currency: "USD" }, 美国: { code: "US", currency: "USD" }, uk: { code: "GB", currency: "GBP" }, gb: { code: "GB", currency: "GBP" }, england: { code: "GB", currency: "GBP" }, "united kingdom": { code: "GB", currency: "GBP" }, 英国: { code: "GB", currency: "GBP" }, ca: { code: "CA", currency: "CAD" }, canada: { code: "CA", currency: "CAD" }, 加拿大: { code: "CA", currency: "CAD" }, au: { code: "AU", currency: "AUD" }, australia: { code: "AU", currency: "AUD" }, 澳大利亚: { code: "AU", currency: "AUD" }, sg: { code: "SG", currency: "SGD" }, singapore: { code: "SG", currency: "SGD" }, 新加坡: { code: "SG", currency: "SGD" }, hk: { code: "HK", currency: "HKD" }, "hong kong": { code: "HK", currency: "HKD" }, 香港: { code: "HK", currency: "HKD" }, jp: { code: "JP", currency: "JPY" }, japan: { code: "JP", currency: "JPY" }, 日本: { code: "JP", currency: "JPY" }, de: { code: "DE", currency: "EUR" }, germany: { code: "DE", currency: "EUR" }, 德国: { code: "DE", currency: "EUR" }, fr: { code: "FR", currency: "EUR" }, france: { code: "FR", currency: "EUR" }, 法国: { code: "FR", currency: "EUR" }, it: { code: "IT", currency: "EUR" }, italy: { code: "IT", currency: "EUR" }, 意大利: { code: "IT", currency: "EUR" }, es: { code: "ES", currency: "EUR" }, spain: { code: "ES", currency: "EUR" }, 西班牙: { code: "ES", currency: "EUR" }, }; let selectedPlan = "plus"; let generatedLink = ""; let isGeneratingCheckoutLink = false; let paymentAccounts = []; const bindTaskState = { page: 1, pageSize: 50, status: "", search: "", }; let bindTaskAutoRefreshTimer = null; let billingBatchProfiles = []; function formatErrorMessage(error) { if (!error) return "未知错误"; if (typeof error === "string") return error; // ApiClient 会把后端错误挂在 error.data 上 const detail = error?.data?.detail ?? error?.message; if (typeof detail === "string" && detail && detail !== "[object Object]") { return detail; } try { return JSON.stringify(detail || error); } catch { return String(detail || error); } } function escapeHtml(value) { const div = document.createElement("div"); div.textContent = String(value ?? ""); return div.innerHTML; } function yesNo(value) { return value ? "是" : "否"; } function showSessionDiagnosticPanel(text) { const panel = document.getElementById("session-diagnostic-panel"); const pre = document.getElementById("session-diagnostic-text"); if (!panel || !pre) return; pre.textContent = String(text || ""); panel.classList.add("show"); } function clearSessionDiagnosticPanel() { const panel = document.getElementById("session-diagnostic-panel"); const pre = document.getElementById("session-diagnostic-text"); if (!panel || !pre) return; pre.textContent = ""; panel.classList.remove("show"); } function formatSessionDiagnosticPayload(payload) { if (!payload || typeof payload !== "object") { return "诊断结果为空"; } const token = payload.token_state || {}; const cookie = payload.cookie_state || {}; const bootstrap = payload.bootstrap_capability || {}; const probe = payload.probe || null; const notes = Array.isArray(payload.notes) ? payload.notes : []; const lines = [ `账号: ${payload.email || "-"} (ID=${payload.account_id || "-"})`, `Access Token: ${yesNo(token.has_access_token)} | len=${token.access_token_len || 0} | ${token.access_token_preview || "-"}`, `Refresh Token: ${yesNo(token.has_refresh_token)} | len=${token.refresh_token_len || 0}`, `Session(DB): ${yesNo(token.has_session_token_db)} | len=${token.session_token_db_len || 0} | ${token.session_token_db_preview || "-"}`, `Session(Cookie): ${yesNo(token.has_session_token_cookie)} | len=${token.session_token_cookie_len || 0} | ${token.session_token_cookie_preview || "-"}`, `Session(Resolved): len=${token.resolved_session_token_len || 0} | ${token.resolved_session_token_preview || "-"}`, `Cookies: ${yesNo(cookie.has_cookies)} | len=${cookie.cookies_len || 0}`, `oai-did: ${yesNo(cookie.has_oai_did)} | ${cookie.resolved_oai_did || "-"}`, `Session 分片: count=${cookie.session_chunk_count || 0} | [${(cookie.session_chunk_indices || []).join(", ")}]`, `自动补会话能力: ${yesNo(bootstrap.can_login_bootstrap)} | has_password=${yesNo(bootstrap.has_password)} | email_service=${bootstrap.email_service_type || "-"}`, ]; if (probe) { lines.push( `实时探测: ok=${yesNo(probe.ok)} | http=${probe.http_status ?? "-"} | session=${yesNo(probe.session_token_found)} | session_json_access=${yesNo(probe.access_token_in_session_json)}` ); if (probe.session_token_preview) { lines.push(`探测 session 预览: ${probe.session_token_preview}`); } if (probe.access_token_preview) { lines.push(`探测 access 预览: ${probe.access_token_preview}`); } if (probe.error) { lines.push(`探测错误: ${probe.error}`); } } if (notes.length) { lines.push("诊断备注:"); notes.forEach((n) => lines.push(`- ${n}`)); } if (payload.recommendation) { lines.push(`建议: ${payload.recommendation}`); } if (payload.checked_at) { lines.push(`检查时间: ${payload.checked_at}`); } return lines.join("\n"); } async function runSessionDiagnostic() { const accountId = Number(document.getElementById("account-select")?.value || 0); if (!accountId) { toast.warning("请先选择账号"); return; } setButtonLoading("session-diagnostic-btn", "诊断中...", true); showSessionDiagnosticPanel("正在诊断会话上下文,请稍候..."); try { const data = await api.get(`/payment/accounts/${accountId}/session-diagnostic?probe=1`); const diag = data?.diagnostic || {}; showSessionDiagnosticPanel(formatSessionDiagnosticPayload(diag)); toast.success("会话诊断完成"); } catch (error) { const message = formatErrorMessage(error); showSessionDiagnosticPanel(`会话诊断失败: ${message}`); toast.error(`会话诊断失败: ${message}`); } finally { setButtonLoading("session-diagnostic-btn", "诊断中...", false); } } async function runSessionBootstrap() { const accountId = Number(document.getElementById("account-select")?.value || 0); if (!accountId) { toast.warning("请先选择账号"); return; } setButtonLoading("session-bootstrap-btn", "补全中...", true); showSessionDiagnosticPanel("正在执行会话补全,请稍候(可能需要等待邮箱验证码)..."); try { const data = await api.post(`/payment/accounts/${accountId}/session-bootstrap`, {}); if (data?.success) { toast.success(`会话补全成功(len=${data?.session_token_len || 0})`); } else { toast.warning(data?.message || "会话补全未命中"); } await runSessionDiagnostic(); } catch (error) { const message = formatErrorMessage(error); showSessionDiagnosticPanel(`会话补全失败: ${message}`); toast.error(`会话补全失败: ${message}`); } finally { setButtonLoading("session-bootstrap-btn", "补全中...", false); } } async function saveManualSessionToken() { const accountId = Number(document.getElementById("account-select")?.value || 0); if (!accountId) { toast.warning("请先选择账号"); return; } const sessionToken = String(document.getElementById("manual-session-token-input")?.value || "").trim(); if (!sessionToken) { toast.warning("请先粘贴 session_token"); return; } setButtonLoading("save-session-token-btn", "保存中...", true); try { const data = await api.post(`/payment/accounts/${accountId}/session-token`, { session_token: sessionToken, merge_cookie: true, }); if (data?.success) { toast.success(`Session Token 已保存(len=${data?.session_token_len || 0})`); await runSessionDiagnostic(); return; } toast.warning(data?.message || "保存完成,但未返回 success"); } catch (error) { toast.error(`保存 Session Token 失败: ${formatErrorMessage(error)}`); } finally { setButtonLoading("save-session-token-btn", "保存中...", false); } } function getInputValue(id) { return (document.getElementById(id)?.value || "").trim(); } function setInputValue(id, value) { const el = document.getElementById(id); if (!el) return; el.value = value ?? ""; } function maskCardNumber(cardNumber) { const digits = String(cardNumber || "").replace(/\D/g, ""); if (!digits) return "-"; if (digits.length <= 8) return `${digits.slice(0, 2)}****${digits.slice(-2)}`; return `${digits.slice(0, 4)}****${digits.slice(-4)}`; } function resolveCountryAlias(raw) { const key = String(raw || "").trim(); if (!key) return null; const normalized = key.toLowerCase(); if (COUNTRY_ALIAS_MAP[normalized]) { return COUNTRY_ALIAS_MAP[normalized]; } const upper = key.toUpperCase(); if (BILLING_COUNTRY_CURRENCY_MAP[upper]) { return { code: upper, currency: BILLING_COUNTRY_CURRENCY_MAP[upper], }; } return null; } function normalizeMonth(value) { const digits = String(value || "").replace(/\D/g, ""); if (!digits) return ""; return digits.slice(0, 2).padStart(2, "0"); } function normalizeYear(value) { const digits = String(value || "").replace(/\D/g, ""); if (!digits) return ""; if (digits.length === 2) return `20${digits}`; return digits.slice(0, 4); } function formatExpiryInput(month, year) { const mm = normalizeMonth(month); const rawYear = String(year || "").replace(/\D/g, ""); const yy = rawYear ? rawYear.slice(-2) : ""; if (!mm && !yy) return ""; return yy ? `${mm}/${yy}` : mm; } function parseExpiryInput(value) { const raw = String(value || "").trim(); if (!raw) { return { exp_month: "", exp_year: "" }; } const slashMatch = raw.match(/^(\d{1,2})\s*\/\s*(\d{1,4})$/); if (slashMatch) { return { exp_month: normalizeMonth(slashMatch[1]), exp_year: normalizeYear(slashMatch[2]), }; } const digits = raw.replace(/\D/g, ""); if (!digits) { return { exp_month: "", exp_year: "" }; } if (digits.length <= 2) { return { exp_month: normalizeMonth(digits), exp_year: "" }; } return { exp_month: normalizeMonth(digits.slice(0, 2)), exp_year: normalizeYear(digits.slice(2)), }; } function normalizeExpiryInputForTyping(value) { const digits = String(value || "").replace(/\D/g, "").slice(0, 6); if (!digits) return ""; if (digits.length <= 2) return digits; return `${digits.slice(0, 2)}/${digits.slice(2)}`; } function parseCardText(rawText) { const text = String(rawText || "").trim(); if (!text) return {}; const lines = text .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); const kv = {}; for (const line of lines) { const match = line.match(/^(.+?)\s*[::]\s*(.+)$/); if (match) { kv[match[1].trim().toLowerCase()] = match[2].trim(); } } const result = {}; // 卡号 const cardKeyCandidates = [ "卡号", "card number", "card", "card_number", ]; for (const key of cardKeyCandidates) { if (!kv[key]) continue; const digits = kv[key].replace(/\D/g, ""); if (digits.length >= 13 && digits.length <= 19) { result.card_number = digits; break; } } if (!result.card_number) { for (const line of lines) { const m = line.replace(/-/g, " ").match(/^(\d{13,19})\s+(0[1-9]|1[0-2])\s+(\d{2,4})\s+(\d{3,4})$/); if (m) { result.card_number = m[1]; result.exp_month = normalizeMonth(m[2]); result.exp_year = normalizeYear(m[3]); result.cvv = m[4]; break; } } } if (!result.card_number) { for (const line of lines) { const digits = line.replace(/\D/g, ""); if (digits.length >= 13 && digits.length <= 19) { result.card_number = digits; break; } } } // 有效期 const expiryKeyCandidates = ["有效期", "exp", "expiry", "expiration", "exp_date"]; for (const key of expiryKeyCandidates) { if (!kv[key]) continue; const value = kv[key]; let m = value.match(/(0[1-9]|1[0-2])\s*\/\s*(\d{2,4})/); if (m) { result.exp_month = normalizeMonth(m[1]); result.exp_year = normalizeYear(m[2]); break; } m = value.match(/^(0[1-9]|1[0-2])(\d{2,4})$/); if (m) { result.exp_month = normalizeMonth(m[1]); result.exp_year = normalizeYear(m[2]); break; } } if (!result.exp_month || !result.exp_year) { for (const line of lines) { const m = line.match(/\b(0[1-9]|1[0-2])\s*\/\s*(\d{2,4})\b/); if (m) { result.exp_month = normalizeMonth(m[1]); result.exp_year = normalizeYear(m[2]); break; } } } // CVV const cvvKeyCandidates = ["cvv", "cvc", "安全码"]; for (const key of cvvKeyCandidates) { if (!kv[key]) continue; const m = kv[key].match(/\b(\d{3,4})\b/); if (m) { result.cvv = m[1]; break; } } if (!result.cvv) { for (let i = 0; i < lines.length; i += 1) { const line = lines[i]; if (!/(cvv|cvc|安全码)/i.test(line)) continue; const direct = line.match(/\b(\d{3,4})\b/); if (direct) { result.cvv = direct[1]; break; } const next = lines[i + 1] || ""; const m = next.match(/\b(\d{3,4})\b/); if (m) { result.cvv = m[1]; break; } } } // 姓名 const nameKeyCandidates = ["姓名", "name", "cardholder", "持卡人"]; for (const key of nameKeyCandidates) { if (kv[key]) { result.billing_name = kv[key]; break; } } if (!result.billing_name) { for (const line of lines) { if (/^[A-Z][a-z]+(\s+[A-Z][a-z]+){0,4}$/.test(line)) { result.billing_name = line; break; } } } // 地址字段 const addressLine = kv["地址"] || kv["address"] || kv["address_line1"] || ""; const city = kv["城市"] || kv["city"] || ""; const state = kv["州"] || kv["state"] || kv["省"] || ""; const postal = kv["邮编"] || kv["postal_code"] || kv["zip"] || kv["zipcode"] || kv["zip_code"] || ""; const countryRaw = kv["国家"] || kv["country"] || kv["地区"] || ""; if (addressLine) result.address_line1 = addressLine; if (city) result.address_city = city; if (state) result.address_state = state; if (postal) result.postal_code = postal; if (countryRaw) { const country = resolveCountryAlias(countryRaw); if (country) { result.country_code = country.code; result.currency = country.currency; } } // 账单地址单行模式 if (!result.address_line1) { let addressCandidate = ""; for (const line of lines) { if (/(账单地址|billing\s*address)/i.test(line)) { addressCandidate = line.replace(/^.*?(账单地址|billing\s*address)\s*[::]?\s*/i, "").trim(); if (addressCandidate) break; } } if (!addressCandidate) { addressCandidate = lines.find((line) => line.includes(",") && /\d/.test(line)) || ""; } if (addressCandidate) { result.raw_address = addressCandidate; const parts = addressCandidate.split(",").map((item) => item.trim()).filter(Boolean); if (parts.length) { result.address_line1 = parts[0]; } if (!result.postal_code) { const zip = addressCandidate.match(/\b(\d{5}(?:-\d{4})?)\b/); if (zip) result.postal_code = zip[1]; } if (!result.address_state) { const stateCode = addressCandidate.match(/\b([A-Z]{2})\b/); if (stateCode) result.address_state = stateCode[1]; } const suffix = parts[parts.length - 1]; if (!result.country_code && suffix) { const country = resolveCountryAlias(suffix); if (country) { result.country_code = country.code; result.currency = country.currency; } } } } return result; } function buildParsedSummary(parsed) { const parts = []; if (parsed.card_number) parts.push(`卡号: ${maskCardNumber(parsed.card_number)}`); if (parsed.exp_month || parsed.exp_year) { parts.push(`有效期: ${formatExpiryInput(parsed.exp_month, parsed.exp_year) || "--/--"}`); } if (parsed.cvv) parts.push("CVV: ***"); if (parsed.billing_name) parts.push(`姓名: ${parsed.billing_name}`); if (parsed.address_line1 || parsed.raw_address) { parts.push(`地址: ${parsed.raw_address || parsed.address_line1}`); } if (parsed.country_code) parts.push(`国家: ${parsed.country_code}`); return parts; } function fillBillingForm(parsed) { if (!parsed || typeof parsed !== "object") return; if (parsed.card_number) setInputValue("card-number-input", parsed.card_number); if (parsed.exp_month || parsed.exp_year) { setInputValue("card-expiry-input", formatExpiryInput(parsed.exp_month, parsed.exp_year)); } if (parsed.cvc || parsed.cvv) setInputValue("card-cvc-input", String(parsed.cvc || parsed.cvv || "")); if (parsed.billing_name) setInputValue("billing-name-input", parsed.billing_name); if (parsed.address_line1) setInputValue("billing-line1-input", parsed.address_line1); if (parsed.address_city) setInputValue("billing-city-input", parsed.address_city); if (parsed.address_state) setInputValue("billing-state-input", parsed.address_state); if (parsed.postal_code) setInputValue("billing-postal-input", parsed.postal_code); if (parsed.country_code) { const countryEl = document.getElementById("billing-country-input"); if (countryEl) countryEl.value = parsed.country_code; } if (parsed.currency) { setInputValue("billing-currency-input", parsed.currency); } else { onBillingCountryChanged(); } persistBillingProfileNonSensitive(); } function onBillingCountryChanged() { const country = (document.getElementById("billing-country-input")?.value || "US").toUpperCase(); const currencyEl = document.getElementById("billing-currency-input"); if (!currencyEl) return; if (!currencyEl.value || currencyEl.value === "USD" || currencyEl.value.length < 3) { currencyEl.value = BILLING_COUNTRY_CURRENCY_MAP[country] || "USD"; } } function setRandomBillingHint(message, mode = "info") { const hintEl = document.getElementById("random-billing-hint"); if (!hintEl) return; hintEl.textContent = String(message || ""); if (mode === "success") { hintEl.style.color = "var(--success-color)"; return; } if (mode === "error") { hintEl.style.color = "var(--danger-color)"; return; } hintEl.style.color = "var(--text-secondary)"; } async function randomBillingByCountry() { const country = String(getInputValue("billing-country-input") || "US").toUpperCase(); setButtonLoading("random-billing-btn", "生成中...", true); setRandomBillingHint("正在获取随机账单资料...", "info"); try { const data = await api.get(`/payment/random-billing?country=${encodeURIComponent(country)}`); const profile = data?.profile || {}; if (!profile || typeof profile !== "object") { throw new Error("返回的账单资料格式无效"); } fillBillingForm({ billing_name: profile.billing_name || "", country_code: profile.country_code || country, currency: profile.currency || "", address_line1: profile.address_line1 || "", address_city: profile.address_city || "", address_state: profile.address_state || "", postal_code: profile.postal_code || "", }); const source = String(profile.source || "unknown"); let sourceLabel = "本地兜底"; if (source === "meiguodizhi") sourceLabel = "meiguodizhi"; if (source === "local_geo") sourceLabel = "本地生成"; if (source === "local_geo_fallback") sourceLabel = "本地兜底"; const fallbackReason = String(profile.fallback_reason || "").trim(); const city = String(profile.address_city || "-"); const state = String(profile.address_state || "-"); const postal = String(profile.postal_code || "-"); if ((source === "local_fallback" || source === "local_geo_fallback") && fallbackReason) { setRandomBillingHint(`来源: ${sourceLabel} | ${city}, ${state}, ${postal} | 外部失败: ${fallbackReason}`, "error"); toast.warning(`外部地址源不可用,已切换本地兜底:${fallbackReason}`); } else { setRandomBillingHint(`来源: ${sourceLabel} | ${city}, ${state}, ${postal}`, "success"); toast.success(`已按 ${country} 随机填充账单资料(${sourceLabel})`); } } catch (error) { setRandomBillingHint(`随机失败: ${formatErrorMessage(error)}`, "error"); toast.error(`随机账单资料失败: ${formatErrorMessage(error)}`); } finally { setButtonLoading("random-billing-btn", "生成中...", false); } } function collectBillingFormData() { const expiry = parseExpiryInput(getInputValue("card-expiry-input")); return { card_number: getInputValue("card-number-input").replace(/\D/g, ""), exp_month: expiry.exp_month, exp_year: expiry.exp_year, cvc: getInputValue("card-cvc-input").replace(/\D/g, ""), billing_name: getInputValue("billing-name-input"), country_code: getInputValue("billing-country-input").toUpperCase(), currency: getInputValue("billing-currency-input").toUpperCase(), address_line1: getInputValue("billing-line1-input"), address_city: getInputValue("billing-city-input"), address_state: getInputValue("billing-state-input"), postal_code: getInputValue("billing-postal-input"), }; } function persistBillingProfileNonSensitive() { const data = collectBillingFormData(); storage.set(BILLING_STORAGE_KEY, { billing_name: data.billing_name, country_code: data.country_code, currency: data.currency, address_line1: data.address_line1, address_city: data.address_city, address_state: data.address_state, postal_code: data.postal_code, }); } function restoreBillingProfileNonSensitive() { const saved = storage.get(BILLING_STORAGE_KEY, null); if (!saved || typeof saved !== "object") { setInputValue("billing-country-input", "US"); setInputValue("billing-currency-input", "USD"); return; } if (saved.billing_name) setInputValue("billing-name-input", saved.billing_name); if (saved.country_code) setInputValue("billing-country-input", saved.country_code); if (saved.currency) setInputValue("billing-currency-input", saved.currency); if (saved.address_line1) setInputValue("billing-line1-input", saved.address_line1); if (saved.address_city) setInputValue("billing-city-input", saved.address_city); if (saved.address_state) setInputValue("billing-state-input", saved.address_state); if (saved.postal_code) setInputValue("billing-postal-input", saved.postal_code); onBillingCountryChanged(); } function getBillingTemplates() { const raw = storage.get(BILLING_TEMPLATE_STORAGE_KEY, []); if (!Array.isArray(raw)) return []; return raw .filter((item) => item && typeof item === "object" && item.id && item.name && item.data) .slice(0, BILLING_TEMPLATE_MAX); } function saveBillingTemplates(list) { const safeList = Array.isArray(list) ? list.slice(0, BILLING_TEMPLATE_MAX) : []; storage.set(BILLING_TEMPLATE_STORAGE_KEY, safeList); } function normalizeTemplateData(data) { const source = data && typeof data === "object" ? data : {}; return { card_number: String(source.card_number || "").replace(/\D/g, ""), exp_month: normalizeMonth(source.exp_month), exp_year: normalizeYear(source.exp_year), cvc: String(source.cvc || source.cvv || "").replace(/\D/g, "").slice(0, 4), billing_name: String(source.billing_name || "").trim(), country_code: String(source.country_code || "US").trim().toUpperCase() || "US", currency: String(source.currency || "").trim().toUpperCase(), address_line1: String(source.address_line1 || "").trim(), address_city: String(source.address_city || "").trim(), address_state: String(source.address_state || "").trim(), postal_code: String(source.postal_code || "").trim(), }; } function refreshBillingTemplateSelect(selectedId = "") { const selectEl = document.getElementById("billing-template-select"); if (!selectEl) return; const templates = getBillingTemplates(); const options = ['']; templates.forEach((tpl) => { options.push(``); }); selectEl.innerHTML = options.join(""); if (selectedId && templates.some((tpl) => tpl.id === selectedId)) { selectEl.value = selectedId; } } function saveCurrentAsTemplate() { const name = getInputValue("billing-template-name"); if (!name) { toast.warning("请先填写模板名称"); return; } const form = normalizeTemplateData(collectBillingFormData()); const hasValue = Boolean( form.card_number || (form.exp_month && form.exp_year) || form.cvc || form.billing_name || form.address_line1 ); if (!hasValue) { toast.warning("当前表单为空,无法保存模板"); return; } const templates = getBillingTemplates(); const normalizedName = name.toLowerCase(); const existing = templates.find((tpl) => String(tpl.name || "").toLowerCase() === normalizedName); const nowIso = new Date().toISOString(); if (existing) { existing.data = form; existing.updated_at = nowIso; saveBillingTemplates(templates); refreshBillingTemplateSelect(existing.id); toast.success(`模板已更新: ${name}`); return; } const newTemplate = { id: `tpl_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`, name, data: form, created_at: nowIso, updated_at: nowIso, }; templates.unshift(newTemplate); saveBillingTemplates(templates); refreshBillingTemplateSelect(newTemplate.id); toast.success(`模板已保存: ${name}`); } function applySelectedTemplate() { const selectEl = document.getElementById("billing-template-select"); if (!selectEl || !selectEl.value) { toast.warning("请先选择模板"); return; } const templates = getBillingTemplates(); const selected = templates.find((tpl) => tpl.id === selectEl.value); if (!selected) { toast.warning("模板不存在或已删除"); refreshBillingTemplateSelect(); return; } fillBillingForm(selected.data || {}); setInputValue("billing-template-name", selected.name || ""); toast.success(`已应用模板: ${selected.name}`); } async function deleteSelectedTemplate() { const selectEl = document.getElementById("billing-template-select"); if (!selectEl || !selectEl.value) { toast.warning("请先选择模板"); return; } const templates = getBillingTemplates(); const selected = templates.find((tpl) => tpl.id === selectEl.value); if (!selected) { toast.warning("模板不存在或已删除"); refreshBillingTemplateSelect(); return; } const ok = await confirm(`确认删除模板「${selected.name}」吗?`, "删除模板"); if (!ok) return; const next = templates.filter((tpl) => tpl.id !== selected.id); saveBillingTemplates(next); refreshBillingTemplateSelect(); setInputValue("billing-template-name", ""); toast.success(`模板已删除: ${selected.name}`); } function saveBatchProfilesAsTemplates() { if (!billingBatchProfiles.length) { toast.warning("请先批量识别文本"); return; } const templates = getBillingTemplates(); const now = new Date(); let saved = 0; billingBatchProfiles.forEach((item, idx) => { const parsed = item?.parsed || {}; const data = normalizeTemplateData({ card_number: parsed.card_number, exp_month: parsed.exp_month, exp_year: parsed.exp_year, cvc: parsed.cvv, billing_name: parsed.billing_name, country_code: parsed.country_code, currency: parsed.currency, address_line1: parsed.address_line1, address_city: parsed.address_city, address_state: parsed.address_state, postal_code: parsed.postal_code, }); const hasValue = Boolean( data.card_number || (data.exp_month && data.exp_year) || data.cvc || data.billing_name || data.address_line1 ); if (!hasValue) return; const suffix = data.card_number ? data.card_number.slice(-4) : String(idx + 1).padStart(2, "0"); const name = `批量模板-${now.toISOString().slice(0, 10)}-${suffix}`; templates.unshift({ id: `tpl_${Date.now()}_${Math.random().toString(16).slice(2, 8)}_${idx}`, name, data, created_at: now.toISOString(), updated_at: now.toISOString(), }); saved += 1; }); if (!saved) { toast.warning("批量记录里没有可保存的模板"); return; } saveBillingTemplates(templates); refreshBillingTemplateSelect(); toast.success(`已保存 ${saved} 个模板`); } function setParseResult(message, type = "info") { const resultEl = document.getElementById("billing-parse-result"); if (!resultEl) return; resultEl.textContent = message || ""; if (type === "error") { resultEl.style.color = "var(--danger-color)"; } else if (type === "success") { resultEl.style.color = "var(--success-color)"; } else { resultEl.style.color = "var(--text-secondary)"; } } function parseSingleBillingText() { const text = getInputValue("billing-paste-text"); if (!text) { setParseResult("请先粘贴文本", "error"); return; } const parsed = parseCardText(text); const summary = buildParsedSummary(parsed); if (!summary.length) { setParseResult("未识别到可用信息,请检查文本格式", "error"); return; } fillBillingForm(parsed); setParseResult(`识别成功: ${summary.join(" | ")}`, "success"); } function splitBatchBlocks(rawText) { const blocks = String(rawText || "") .split(/\n\s*\n+/) .map((part) => part.trim()) .filter(Boolean); if (blocks.length > 1) return blocks; return String(rawText || "") .split(/(?:^-{3,}|^={3,})/m) .map((part) => part.trim()) .filter(Boolean); } function renderBatchProfiles() { const wrap = document.getElementById("billing-batch-wrap"); const summary = document.getElementById("billing-batch-summary"); const tbody = document.getElementById("billing-batch-table"); if (!wrap || !summary || !tbody) return; if (!billingBatchProfiles.length) { wrap.style.display = "none"; tbody.innerHTML = ""; summary.textContent = ""; return; } wrap.style.display = ""; summary.textContent = `已识别 ${billingBatchProfiles.length} 条资料,点击“填充”可写入下方表单。`; tbody.innerHTML = billingBatchProfiles.map((item, index) => { const parsed = item.parsed || {}; const address = parsed.raw_address || [ parsed.address_line1, parsed.address_city, parsed.address_state, parsed.postal_code, ].filter(Boolean).join(", "); return ` ${index + 1} ${escapeHtml(maskCardNumber(parsed.card_number))} ${escapeHtml(formatExpiryInput(parsed.exp_month, parsed.exp_year) || "-")} ${escapeHtml(parsed.cvv ? "***" : "-")} ${escapeHtml(parsed.country_code || "-")} ${escapeHtml(address || "-")} `; }).join(""); } function parseBatchBillingText() { const text = getInputValue("billing-paste-text"); if (!text) { setParseResult("请先粘贴文本", "error"); return; } const blocks = splitBatchBlocks(text); if (!blocks.length) { setParseResult("未检测到可解析的文本块", "error"); return; } billingBatchProfiles = blocks .map((blockText) => ({ raw: blockText, parsed: parseCardText(blockText) })) .filter((item) => { const parsed = item.parsed || {}; return Boolean( parsed.card_number || parsed.exp_month || parsed.exp_year || parsed.cvv || parsed.address_line1 || parsed.raw_address ); }); renderBatchProfiles(); if (!billingBatchProfiles.length) { setParseResult("批量识别完成,但没有可用记录", "error"); return; } setParseResult(`批量识别成功:共 ${billingBatchProfiles.length} 条`, "success"); } function fillFromBatchProfile(index) { const item = billingBatchProfiles[index]; if (!item) return; fillBillingForm(item.parsed || {}); const summary = buildParsedSummary(item.parsed || {}); setParseResult(`已填充第 ${index + 1} 条:${summary.join(" | ")}`, "success"); } function clearBillingText() { setInputValue("billing-paste-text", ""); billingBatchProfiles = []; renderBatchProfiles(); setParseResult("", "info"); } function bindBillingEvents() { document.getElementById("parse-billing-btn")?.addEventListener("click", parseSingleBillingText); document.getElementById("parse-batch-btn")?.addEventListener("click", parseBatchBillingText); document.getElementById("clear-billing-btn")?.addEventListener("click", clearBillingText); document.getElementById("save-billing-template-btn")?.addEventListener("click", saveCurrentAsTemplate); document.getElementById("apply-billing-template-btn")?.addEventListener("click", applySelectedTemplate); document.getElementById("delete-billing-template-btn")?.addEventListener("click", deleteSelectedTemplate); document.getElementById("save-batch-template-btn")?.addEventListener("click", saveBatchProfilesAsTemplates); document.getElementById("billing-template-select")?.addEventListener("change", (event) => { const selectValue = event?.target?.value || ""; if (!selectValue) { setInputValue("billing-template-name", ""); return; } const selected = getBillingTemplates().find((tpl) => tpl.id === selectValue); if (selected) { setInputValue("billing-template-name", selected.name || ""); } }); document.getElementById("billing-country-input")?.addEventListener("change", () => { onBillingCountryChanged(); persistBillingProfileNonSensitive(); setRandomBillingHint(""); }); [ "card-number-input", "card-expiry-input", "card-cvc-input", "billing-name-input", "billing-country-input", "billing-currency-input", "billing-line1-input", "billing-city-input", "billing-state-input", "billing-postal-input", ].forEach((id) => { const node = document.getElementById(id); node?.addEventListener("input", debounce(() => { persistBillingProfileNonSensitive(); resetGenerateLinkButtonState(); }, 200)); node?.addEventListener("change", resetGenerateLinkButtonState); }); document.getElementById("card-expiry-input")?.addEventListener("input", (event) => { const el = event.target; if (!el) return; const next = normalizeExpiryInputForTyping(el.value); if (el.value !== next) { el.value = next; } }); document.getElementById("card-number-input")?.addEventListener("input", (event) => { const el = event.target; if (!el) return; const digits = String(el.value || "").replace(/\D/g, "").slice(0, 19); const grouped = digits.replace(/(\d{4})(?=\d)/g, "$1 ").trim(); if (el.value !== grouped) { el.value = grouped; } }); document.getElementById("card-cvc-input")?.addEventListener("input", (event) => { const el = event.target; if (!el) return; const digits = String(el.value || "").replace(/\D/g, "").slice(0, 4); if (el.value !== digits) { el.value = digits; } }); } function getTaskStatusText(status) { const mapping = { link_ready: "待打开", opened: "已打开", waiting_user_action: "待用户完成", paid_pending_sync: "已支付待同步", verifying: "验证中", completed: "已完成", failed: "失败", }; return mapping[status] || status || "-"; } function startBindTaskAutoRefresh() { stopBindTaskAutoRefresh(); bindTaskAutoRefreshTimer = setInterval(() => { const bindTaskTab = document.getElementById("tab-content-bind-task"); if (!bindTaskTab?.classList.contains("active")) return; loadBindCardTasks(true); }, 20000); } function stopBindTaskAutoRefresh() { if (!bindTaskAutoRefreshTimer) return; clearInterval(bindTaskAutoRefreshTimer); bindTaskAutoRefreshTimer = null; } function setButtonLoading(buttonId, loadingText, isLoading) { const btn = document.getElementById(buttonId); if (!btn) return; if (isLoading) { if (!btn.dataset.originalText) { btn.dataset.originalText = btn.textContent || ""; } btn.disabled = true; btn.textContent = loadingText; return; } btn.disabled = false; btn.textContent = btn.dataset.originalText || btn.textContent; } function resetGenerateLinkButtonState() { const btn = document.getElementById("generate-link-btn"); if (!btn) return; btn.disabled = false; if (btn.dataset.originalText) { btn.textContent = btn.dataset.originalText; } } function getBindMode() { return (document.getElementById("bind-mode-select")?.value || "semi_auto").trim() || "semi_auto"; } function updateSemiAutoActionsVisibility(mode) { const isSemiAuto = (mode || getBindMode()) === "semi_auto"; const loginBtn = document.getElementById("semi-login-gpt-btn"); const emailBtn = document.getElementById("semi-account-email-btn"); if (loginBtn) { loginBtn.style.display = isSemiAuto ? "" : "none"; } if (emailBtn) { emailBtn.style.display = isSemiAuto ? "" : "none"; } } function getSelectedAccountEmail() { const accountId = Number(document.getElementById("account-select")?.value || 0); if (!accountId) return ""; const matched = (paymentAccounts || []).find((acc) => Number(acc?.id || 0) === accountId); return String(matched?.email || "").trim(); } function updateSelectedAccountEmailLabel() { const emailBtn = document.getElementById("semi-account-email-btn"); if (!emailBtn) return; const email = getSelectedAccountEmail(); emailBtn.textContent = "邮箱"; emailBtn.title = email ? `当前账号: ${email}(点击查询最新验证码)` : "请先选择账号"; } async function openGptOfficialLogin() { const accountId = Number(document.getElementById("account-select")?.value || 0); if (!accountId) { toast.warning("请先选择账号"); return; } const loginUrl = "https://chatgpt.com/auth/login"; try { const data = await api.post("/payment/open-incognito", { url: loginUrl, account_id: accountId, }); if (data?.success) { toast.success("已打开 GPT 官方登录页(无痕)"); return; } window.open(loginUrl, "_blank", "noopener,noreferrer"); toast.warning(data?.message || "未找到可用浏览器,已在当前浏览器打开"); } catch (error) { window.open(loginUrl, "_blank", "noopener,noreferrer"); toast.warning(`打开无痕失败,已在当前浏览器打开: ${formatErrorMessage(error)}`); } } async function fetchSelectedAccountInbox() { const accountId = Number(document.getElementById("account-select")?.value || 0); const email = getSelectedAccountEmail(); if (!accountId || !email) { toast.warning("请先选择账号"); return; } toast.info(`正在查询 ${email} 收件箱...`); try { const result = await api.post(`/accounts/${accountId}/inbox-code`, {}); if (result?.success && result?.code) { const code = String(result.code).trim(); copyToClipboard(code); toast.success(`${email} 最新验证码: ${code}(已复制)`, 8000); return; } toast.error(`查询失败: ${result?.error || "未收到验证码"}`); } catch (error) { toast.error(`查询失败: ${formatErrorMessage(error)}`); } } function onBindModeChange() { const mode = getBindMode(); const thirdPartyPanel = document.getElementById("third-party-config"); if (thirdPartyPanel) { thirdPartyPanel.style.display = mode === "third_party" ? "" : "none"; } const actionBtn = document.getElementById("create-bind-task-btn"); if (actionBtn) { if (mode === "third_party") { actionBtn.textContent = "创建并执行第三方自动绑卡"; } else if (mode === "local_auto") { actionBtn.textContent = "创建并执行全自动绑卡"; } else { actionBtn.textContent = "生成并加入绑卡任务(半自动)"; } } updateSemiAutoActionsVisibility(mode); updateSelectedAccountEmailLabel(); storage.set(BIND_MODE_STORAGE_KEY, mode); } function collectThirdPartyConfig() { const apiUrl = getInputValue("third-party-api-url"); const apiKey = getInputValue("third-party-api-key"); return { api_url: apiUrl, api_key: apiKey }; } function restoreBindModeConfig() { const modeSelect = document.getElementById("bind-mode-select"); const savedMode = String(storage.get(BIND_MODE_STORAGE_KEY, "semi_auto") || "semi_auto"); if (modeSelect) { modeSelect.value = ["semi_auto", "third_party", "local_auto"].includes(savedMode) ? savedMode : "semi_auto"; } const savedApiUrl = String(storage.get(THIRD_PARTY_BIND_URL_STORAGE_KEY, "") || "").trim(); const initialApiUrl = savedApiUrl || THIRD_PARTY_BIND_DEFAULT_URL; setInputValue("third-party-api-url", initialApiUrl); if (!savedApiUrl) { storage.set(THIRD_PARTY_BIND_URL_STORAGE_KEY, initialApiUrl); } onBindModeChange(); } function switchPaymentTab(tab) { const isLink = tab === "link"; document.getElementById("tab-btn-link")?.classList.toggle("active", isLink); document.getElementById("tab-btn-bind-task")?.classList.toggle("active", !isLink); document.getElementById("tab-content-link")?.classList.toggle("active", isLink); document.getElementById("tab-content-bind-task")?.classList.toggle("active", !isLink); if (!isLink) { loadBindCardTasks(true); } } function getCheckoutPayload() { const accountId = Number(document.getElementById("account-select")?.value || 0); if (!accountId) { throw new Error("请先选择账号"); } const payload = { account_id: accountId, plan_type: selectedPlan, country: (document.getElementById("country-select")?.value || "US").toUpperCase(), currency: (document.getElementById("currency-display")?.value || "USD").toUpperCase(), }; if (selectedPlan === "team") { payload.workspace_name = document.getElementById("workspace-name")?.value || "MyTeam"; payload.seat_quantity = Number(document.getElementById("seat-quantity")?.value || 5) || 5; payload.price_interval = document.getElementById("price-interval")?.value || "month"; } return payload; } function showGeneratedLink(data) { generatedLink = data.link || ""; const linkText = document.getElementById("link-text"); const linkBox = document.getElementById("link-box"); const statusEl = document.getElementById("open-status"); if (!linkText || !linkBox || !statusEl) return; linkText.value = generatedLink; linkBox.classList.add("show"); const source = data.source ? `来源: ${data.source}` : ""; const routeHint = data.is_official_checkout ? "已拿到官方 checkout 链接,可直接绑卡" : "当前是中转链接,点击“直接打开支付页”继续跳转"; statusEl.textContent = [source, routeHint].filter(Boolean).join(" | "); } document.addEventListener("DOMContentLoaded", () => { const countrySelect = document.getElementById("country-select"); const currencyDisplay = document.getElementById("currency-display"); if (countrySelect) { countrySelect.value = countrySelect.value || "US"; } if (currencyDisplay) { currencyDisplay.value = currencyDisplay.value || "USD"; } const searchInput = document.getElementById("bind-task-search"); if (searchInput) { searchInput.addEventListener( "input", debounce(() => { bindTaskState.search = (searchInput.value || "").trim(); bindTaskState.page = 1; loadBindCardTasks(); }, 250) ); } const statusSelect = document.getElementById("bind-task-status"); if (statusSelect) { statusSelect.addEventListener("change", () => { bindTaskState.status = statusSelect.value || ""; bindTaskState.page = 1; loadBindCardTasks(); }); } document.getElementById("account-select")?.addEventListener("change", () => { resetGenerateLinkButtonState(); updateSelectedAccountEmailLabel(); }); bindBillingEvents(); restoreBillingProfileNonSensitive(); refreshBillingTemplateSelect(); restoreBindModeConfig(); document.getElementById("third-party-api-url")?.addEventListener( "input", debounce(() => { const apiUrl = getInputValue("third-party-api-url"); storage.set(THIRD_PARTY_BIND_URL_STORAGE_KEY, apiUrl); }, 200) ); document.getElementById("bind-mode-select")?.addEventListener("change", onBindModeChange); loadAccounts(); onCountryChange(); loadBindCardTasks(); startBindTaskAutoRefresh(); switchPaymentTab("link"); window.addEventListener("beforeunload", stopBindTaskAutoRefresh); }); // 加载账号列表 async function loadAccounts() { try { // 后端 page_size 最大为 100,超限会返回 422。 // 这里读取账号管理列表,不按状态硬过滤,避免“有账号但选不到”。 const data = await api.get("/accounts?page=1&page_size=100"); const sel = document.getElementById("account-select"); if (!sel) return; sel.innerHTML = ''; paymentAccounts = Array.isArray(data.accounts) ? data.accounts : []; (data.accounts || []).forEach((acc) => { const opt = document.createElement("option"); opt.value = acc.id; const subText = acc.subscription_type ? ` (${String(acc.subscription_type).toUpperCase()})` : ""; opt.textContent = `${acc.email}${subText}`; sel.appendChild(opt); }); updateSelectedAccountEmailLabel(); updateSemiAutoActionsVisibility(getBindMode()); } catch (e) { toast.error(`加载账号失败: ${formatErrorMessage(e)}`); } } // 国家切换 function onCountryChange() { const country = document.getElementById("country-select")?.value || "US"; const currency = COUNTRY_CURRENCY_MAP[country] || "USD"; const currencyEl = document.getElementById("currency-display"); if (currencyEl) { currencyEl.value = currency; } } // 选择套餐 function selectPlan(plan) { selectedPlan = plan; document.getElementById("plan-plus")?.classList.toggle("selected", plan === "plus"); document.getElementById("plan-team")?.classList.toggle("selected", plan === "team"); document.getElementById("team-options")?.classList.toggle("show", plan === "team"); // 切换套餐时隐藏已有链接,避免误用旧链接。 document.getElementById("link-box")?.classList.remove("show"); generatedLink = ""; resetGenerateLinkButtonState(); } // 生成支付链接 async function generateLink() { if (isGeneratingCheckoutLink) { return; } let payload; try { payload = getCheckoutPayload(); } catch (err) { toast.warning(err.message || "参数不完整"); return; } isGeneratingCheckoutLink = true; setButtonLoading("generate-link-btn", "生成中...", true); try { const data = await api.post("/payment/generate-link", payload); if (!data?.success || !data?.link) { throw new Error(data?.detail || "生成链接失败"); } showGeneratedLink(data); toast.success("支付链接生成成功"); } catch (e) { toast.error(`生成链接失败: ${formatErrorMessage(e)}`); } finally { isGeneratingCheckoutLink = false; setButtonLoading("generate-link-btn", "生成中...", false); resetGenerateLinkButtonState(); } } async function submitThirdPartyAutoBind(task, bindData) { const thirdParty = collectThirdPartyConfig(); const apiUrl = String(thirdParty.api_url || "").trim(); const apiKey = String(thirdParty.api_key || "").trim(); if (apiUrl) { storage.set(THIRD_PARTY_BIND_URL_STORAGE_KEY, apiUrl); } const expYear = String(bindData.exp_year || "").replace(/\D/g, ""); const payload = { api_url: apiUrl || undefined, api_key: apiKey || undefined, timeout_seconds: 180, interval_seconds: 10, card: { number: String(bindData.card_number || "").replace(/\D/g, ""), exp_month: String(bindData.exp_month || "").replace(/\D/g, "").padStart(2, "0").slice(0, 2), exp_year: (expYear.slice(-2) || expYear || "").padStart(2, "0"), cvc: String(bindData.cvc || "").replace(/\D/g, "").slice(0, 4), }, profile: { name: String(bindData.billing_name || "").trim(), email: String(task?.account_email || "").trim() || undefined, country: String(bindData.country_code || "US").toUpperCase(), line1: String(bindData.address_line1 || "").trim(), city: String(bindData.address_city || "").trim(), state: String(bindData.address_state || "").trim(), postal: String(bindData.postal_code || "").trim(), }, }; return api.post(`/payment/bind-card/tasks/${task.id}/auto-bind-third-party`, payload); } async function submitLocalAutoBind(task, bindData) { const expYear = String(bindData.exp_year || "").replace(/\D/g, ""); const payload = { browser_timeout_seconds: 220, post_submit_wait_seconds: 90, verify_timeout_seconds: 180, verify_interval_seconds: 10, headless: false, card: { number: String(bindData.card_number || "").replace(/\D/g, ""), exp_month: String(bindData.exp_month || "").replace(/\D/g, "").padStart(2, "0").slice(0, 2), exp_year: (expYear.slice(-2) || expYear || "").padStart(2, "0"), cvc: String(bindData.cvc || "").replace(/\D/g, "").slice(0, 4), }, profile: { name: String(bindData.billing_name || "").trim(), email: String(task?.account_email || "").trim() || undefined, country: String(bindData.country_code || "US").toUpperCase(), line1: String(bindData.address_line1 || "").trim(), city: String(bindData.address_city || "").trim(), state: String(bindData.address_state || "").trim(), postal: String(bindData.postal_code || "").trim(), }, }; return api.post(`/payment/bind-card/tasks/${task.id}/auto-bind-local`, payload); } async function runLocalAutoBindInBackground(task, bindData) { try { const autoResult = await submitLocalAutoBind(task, bindData); if (autoResult?.verified) { toast.success(`任务 #${task.id} 全自动绑卡完成: ${String(autoResult.subscription_type || "").toUpperCase()}`); } else if (autoResult?.paid_confirmed) { toast.success(`任务 #${task.id} 已确认支付,等待订阅同步(可点“同步订阅”)`, 7000); } else if (autoResult?.pending || autoResult?.need_user_action) { const stage = String(autoResult?.local_auto?.stage || autoResult?.local_auto?.error || "challenge").toUpperCase(); toast.warning( `任务 #${task.id} 全自动绑卡已提交(${stage}),请在支付页完成验证后点击“我已完成支付”或“同步订阅”。`, 9000 ); } else { const sub = String(autoResult?.subscription_type || "free").toUpperCase(); toast.warning(`任务 #${task.id} 全自动绑卡执行完成,但当前订阅为 ${sub},请稍后再同步`, 7000); } } catch (autoErr) { toast.error(`任务 #${task.id} 全自动绑卡失败: ${formatErrorMessage(autoErr)}`); } finally { try { await loadBindCardTasks(); } catch (_) { // 忽略刷新异常,避免覆盖前面的业务提示 } } } // 生成并创建绑卡任务 async function createBindCardTask() { let payload; try { payload = getCheckoutPayload(); } catch (err) { toast.warning(err.message || "参数不完整"); return; } const bindMode = getBindMode(); const bindData = collectBillingFormData(); const missing = []; if (!bindData.card_number) missing.push("卡号"); if (!bindData.exp_month || !bindData.exp_year) missing.push("有效期"); if (!bindData.cvc) missing.push("CVC"); if (!bindData.billing_name) missing.push("姓名"); if (!bindData.address_line1) missing.push("地址"); if (!bindData.postal_code) missing.push("邮编"); if (missing.length && bindMode === "semi_auto") { toast.warning(`绑卡资料未完整:${missing.join("、")}(本次仅创建半自动任务,不会阻断)`, 5000); } if (missing.length && (bindMode === "third_party" || bindMode === "local_auto")) { const modeText = bindMode === "third_party" ? "第三方自动绑卡" : "全自动绑卡"; toast.warning(`${modeText}需要完整资料:${missing.join("、")}`, 5000); return; } payload.auto_open = Boolean(document.getElementById("bind-auto-open")?.checked); payload.bind_mode = bindMode; setButtonLoading("create-bind-task-btn", "创建中...", true); try { const data = await api.post("/payment/bind-card/tasks", payload); if (!data?.success || !data?.task) { throw new Error(data?.detail || "创建绑卡任务失败"); } if (data.link) { showGeneratedLink({ link: data.link, source: data.source, is_official_checkout: data.is_official_checkout, }); } if (bindMode === "third_party") { toast.info(`任务 #${data.task.id} 已创建,正在调用第三方自动绑卡...`, 3000); try { const autoResult = await submitThirdPartyAutoBind(data.task, bindData); if (autoResult?.verified) { toast.success(`任务 #${data.task.id} 自动绑卡完成: ${String(autoResult.subscription_type || "").toUpperCase()}`); } else if (autoResult?.paid_confirmed) { toast.success(`任务 #${data.task.id} 已确认支付,等待订阅同步(可点“同步订阅”)`, 7000); } else if (autoResult?.pending || autoResult?.need_user_action) { const tp = autoResult?.third_party || {}; const assess = tp?.assessment || {}; const snapshot = assess?.snapshot || {}; const paymentStatus = String(snapshot?.payment_status || "").toUpperCase() || "UNKNOWN"; toast.warning( `任务 #${data.task.id} 第三方已受理(payment_status=${paymentStatus}),可能需要 challenge;请在支付页完成后点“我已完成支付”或“同步订阅”。`, 9000 ); } else { const sub = String(autoResult?.subscription_type || "free").toUpperCase(); toast.warning(`任务 #${data.task.id} 第三方提交成功,但当前订阅为 ${sub},请稍后再同步`, 7000); } } catch (autoErr) { toast.error(`任务 #${data.task.id} 第三方自动绑卡失败: ${formatErrorMessage(autoErr)}`); } } else if (bindMode === "local_auto") { toast.info(`任务 #${data.task.id} 已创建,已在后台执行全自动绑卡,可继续修改参数并创建新任务`, 5000); runLocalAutoBindInBackground(data.task, { ...bindData }); } else { toast.success(`绑卡任务已创建 #${data.task.id}${data.auto_opened ? ",浏览器已打开" : ""}`); } switchPaymentTab("bind-task"); await loadBindCardTasks(); } catch (e) { toast.error(`创建绑卡任务失败: ${formatErrorMessage(e)}`); } finally { setButtonLoading("create-bind-task-btn", "创建中...", false); } } function copyLink() { if (!generatedLink) { toast.warning("请先生成链接"); return; } copyToClipboard(generatedLink); } // 在当前浏览器直接打开支付页(适合 Docker/远程部署场景) function openCheckout() { if (!generatedLink) { toast.warning("请先生成链接"); return; } const w = window.open(generatedLink, "_blank", "noopener,noreferrer"); if (!w) { window.location.href = generatedLink; } } // 无痕打开浏览器(携带账号 cookie) async function openIncognito() { if (!generatedLink) { toast.warning("请先生成链接"); return; } const accountId = Number(document.getElementById("account-select")?.value || 0); const statusEl = document.getElementById("open-status"); if (statusEl) { statusEl.textContent = "正在打开..."; } try { const body = { url: generatedLink }; if (accountId) body.account_id = accountId; const data = await api.post("/payment/open-incognito", body); if (data?.success) { if (statusEl) statusEl.textContent = "已在无痕模式打开浏览器"; toast.success("无痕浏览器已打开"); } else { if (statusEl) statusEl.textContent = data?.message || "未找到可用浏览器,请手动复制链接"; toast.warning(data?.message || "未找到可用浏览器"); } } catch (e) { if (statusEl) statusEl.textContent = `请求失败: ${formatErrorMessage(e)}`; toast.error(`请求失败: ${formatErrorMessage(e)}`); } } async function loadBindCardTasks(silent = false) { const tbody = document.getElementById("bind-card-task-table"); if (!tbody) return; if (!silent) { setButtonLoading("refresh-bind-task-btn", "刷新中...", true); } try { const params = new URLSearchParams({ page: String(bindTaskState.page), page_size: String(bindTaskState.pageSize), }); if (bindTaskState.status) params.set("status", bindTaskState.status); if (bindTaskState.search) params.set("search", bindTaskState.search); const data = await api.get(`/payment/bind-card/tasks?${params.toString()}`); const tasks = data?.tasks || []; if (!tasks.length) { tbody.innerHTML = `
暂无绑卡任务
`; return; } tbody.innerHTML = tasks.map((task) => ` ${task.id} ${escapeHtml(task.account_email || "-")} ${String(task.plan_type || "-").toUpperCase()} ${escapeHtml(getTaskStatusText(task.status))} ${escapeHtml(task.checkout_url || "-")} ${escapeHtml(task.checkout_source || "-")} ${format.date(task.created_at)}
${task.last_error ? `
${escapeHtml(task.last_error)}
` : ""} `).join(""); } catch (e) { tbody.innerHTML = `
加载失败: ${escapeHtml(formatErrorMessage(e))}
`; } finally { if (!silent) { setButtonLoading("refresh-bind-task-btn", "刷新中...", false); } } } async function openBindCardTask(taskId) { try { const data = await api.post(`/payment/bind-card/tasks/${taskId}/open`, {}); if (data?.success) { toast.success(`任务 #${taskId} 已尝试打开`); await loadBindCardTasks(); return; } throw new Error(data?.detail || "打开失败"); } catch (e) { toast.error(`打开任务失败: ${formatErrorMessage(e)}`); } } async function markBindCardTaskUserAction(taskId) { try { toast.info(`任务 #${taskId} 正在验证订阅,最多等待 180 秒...`, 3000); const data = await api.post(`/payment/bind-card/tasks/${taskId}/mark-user-action`, { timeout_seconds: 180, interval_seconds: 10, }); if (data?.verified) { toast.success(`任务 #${taskId} 验证成功: ${String(data.subscription_type || "").toUpperCase()}`); } else { const sub = String(data?.subscription_type || "free").toUpperCase(); const source = String(data?.detail?.source || "unknown"); const confidence = String(data?.detail?.confidence || "unknown"); const note = String(data?.detail?.note || ""); const suffix = note ? `, note=${note}` : ""; toast.warning( `任务 #${taskId} 暂未检测到订阅(当前 ${sub}, source=${source}, confidence=${confidence}${suffix}),已切回待用户完成`, 7000 ); } await loadBindCardTasks(); } catch (e) { // 兼容旧后端:如果 mark-user-action 尚未部署,自动降级到 sync-subscription。 const detail = String(e?.data?.detail || "").toLowerCase(); const isRouteNotFound = e?.response?.status === 404 && detail === "not found"; if (isRouteNotFound) { try { const fallback = await api.post(`/payment/bind-card/tasks/${taskId}/sync-subscription`, {}); const sub = String(fallback?.subscription_type || "free").toUpperCase(); if (sub === "PLUS" || sub === "TEAM") { toast.success(`任务 #${taskId} 已通过兼容模式同步成功: ${sub}`); } else { toast.warning(`任务 #${taskId} 兼容同步完成,但当前仍是 ${sub}`, 5000); } await loadBindCardTasks(); return; } catch (fallbackErr) { toast.error(`验证订阅失败(兼容模式也失败): ${formatErrorMessage(fallbackErr)}`); return; } } toast.error(`验证订阅失败: ${formatErrorMessage(e)}`); } } async function syncBindCardTask(taskId) { try { const data = await api.post(`/payment/bind-card/tasks/${taskId}/sync-subscription`, {}); const sub = String(data?.subscription_type || "free").toUpperCase(); const source = String(data?.detail?.source || "unknown"); const confidence = String(data?.detail?.confidence || "unknown"); const note = String(data?.detail?.note || ""); const suffix = note ? `, note=${note}` : ""; const msg = `同步完成: ${sub} (source=${source}, confidence=${confidence}${suffix})`; if (sub === "PLUS" || sub === "TEAM") { toast.success(msg); } else { toast.warning(msg, 7000); } await loadBindCardTasks(); } catch (e) { toast.error(`同步订阅失败: ${formatErrorMessage(e)}`); } } async function deleteBindCardTask(taskId) { const ok = await confirm(`确认删除绑卡任务 #${taskId} 吗?`, "删除任务"); if (!ok) return; try { await api.delete(`/payment/bind-card/tasks/${taskId}`); toast.success(`任务 #${taskId} 已删除`); await loadBindCardTasks(); } catch (e) { toast.error(`删除任务失败: ${formatErrorMessage(e)}`); } } window.selectPlan = selectPlan; window.generateLink = generateLink; window.createBindCardTask = createBindCardTask; window.copyLink = copyLink; window.openCheckout = openCheckout; window.openIncognito = openIncognito; window.onBindModeChange = onBindModeChange; window.switchPaymentTab = switchPaymentTab; window.loadBindCardTasks = loadBindCardTasks; window.openBindCardTask = openBindCardTask; window.markBindCardTaskUserAction = markBindCardTaskUserAction; window.syncBindCardTask = syncBindCardTask; window.deleteBindCardTask = deleteBindCardTask; window.fillFromBatchProfile = fillFromBatchProfile; window.randomBillingByCountry = randomBillingByCountry;