/** * Canonical VIP purchase and order polling coordinator. * * Purchase requests are deduplicated by package/payment method. External * orders use bounded, visibility-aware polling with AbortController support; * terminal states invalidate the user-scoped modal cache exactly once. * * @package AKSRT */ (function (window, document) { "use strict"; var ORDER_PATH = "/pay/content/order/"; var DEFAULT_MAX_ATTEMPTS = 12; var DEFAULT_MAX_ELAPSED = 120000; var MAX_DELAY = 15000; var createFlights = Object.create(null); var polls = Object.create(null); var SUCCESS_STATUSES = { paid: true, delivered: true, completed: true, pending_review: true, }; var FAILURE_STATUSES = { cancelled: true, expired: true, refunded: true, failed: true, }; function userScope() { var config = window.aksrtData || window.aksrt || {}; var userId = parseInt(config.userId || config.user_id || 0, 10) || 0; if (userId > 0) return "user:" + userId; return config.isLoggedIn ? "auth" : "guest"; } function clearFlights() { createFlights = Object.create(null); Object.keys(polls).forEach(function (orderNumber) { var active = polls[orderNumber]; if (active && active.handle && typeof active.handle.abort === "function") active.handle.abort(); }); polls = Object.create(null); } ["aksrt:auth:logout", "aksrt:account-switch", "aksrt:logout", "aksrt:user:logout", "aksrt:auth:changed", "wp_logout"].forEach(function (eventName) { document.addEventListener(eventName, clearFlights); }); function responseData(result) { if (!result) return {}; if (result.data !== undefined && result.data !== null) return result.data || {}; if (result.json && result.json.data !== undefined && result.json.data !== null) return result.json.data || {}; return result.json || {}; } function normalizeError(error) { var response = error && error.response ? error.response : {}; var json = error && error.json ? error.json : (response.json || {}); return { ok: false, status: Number((error && error.status) || response.status || 0), json: json || { message: error && error.message ? error.message : "请求失败" }, }; } function request(path, options) { return window.AksrtRequest.request(path, options || {}).then(function (result) { return { ok: true, status: Number(result.status || 200), json: result.json || {}, data: responseData(result), }; }).catch(normalizeError); } function invalidate(tags) { window.AksrtModalRuntime.invalidate(tags || ["vip", "orders"]); } function callback(options, name) { var args = Array.prototype.slice.call(arguments, 2); if (!options || typeof options[name] !== "function") return; try { options[name].apply(null, args); } catch (error) { if (window.console && typeof window.console.error === "function") { window.console.error("[AKSRT VIP] callback failed", error); } } } function purchase(options) { options = options || {}; var packageId = parseInt(options.packageId || options.package_id || 0, 10); var method = String(options.paymentMethod || options.payment_method || "balance"); if (!packageId) { return Promise.resolve({ ok: false, status: 400, json: { message: "套餐无效" } }); } var key = userScope() + ":" + String(packageId) + ":" + method; if (createFlights[key]) return createFlights[key]; var promise = request("/pay/vip", { method: "POST", body: JSON.stringify({ package_id: packageId, payment_method: method, }), signal: options.signal, }); createFlights[key] = promise; promise.then(function () { if (createFlights[key] === promise) delete createFlights[key]; }); return promise; } function exchange(options) { options = options || {}; var packageId = parseInt(options.packageId || options.package_id || 0, 10); if (!packageId) { return Promise.resolve({ ok: false, status: 400, json: { message: "兑换套餐无效" } }); } return request("/pay/vip/points-exchange", { method: "POST", body: JSON.stringify({ package_id: packageId }), signal: options.signal, }).then(function (response) { if (response.ok) invalidate(options.tags || ["vip", "orders"]); return response; }); } function isTerminal(status, data) { if (data && data.paid) return true; return !!(SUCCESS_STATUSES[status] || FAILURE_STATUSES[status]); } function backoff(attempt) { return Math.min(MAX_DELAY, 1000 * Math.pow(2, Math.max(0, attempt - 1))); } function poll(orderNumber, options) { options = options || {}; orderNumber = String(orderNumber || ""); if (!orderNumber) { return { promise: Promise.resolve({ terminal: true, success: false, status: "invalid" }), abort: function () {}, }; } if (polls[orderNumber]) return polls[orderNumber].handle; var maxAttempts = Math.max(1, Number(options.maxAttempts || DEFAULT_MAX_ATTEMPTS)); var maxElapsed = Math.max(1000, Number(options.maxElapsed || DEFAULT_MAX_ELAPSED)); var state = { attempt: 0, startedAt: Date.now(), timer: null, controller: null, requestTimer: null, waitingForVisible: false, done: false, aborted: false, resolve: null, onVisibility: null, }; state.promise = new Promise(function (resolve) { state.resolve = resolve; }); function finish(result) { if (state.done) return; state.done = true; if (state.timer) { window.clearTimeout(state.timer); state.timer = null; } if (state.requestTimer) { window.clearTimeout(state.requestTimer); state.requestTimer = null; } if (state.controller) { try { state.controller.abort(); } catch (error) {} state.controller = null; } if (state.onVisibility) document.removeEventListener("visibilitychange", state.onVisibility); if (polls[orderNumber] && polls[orderNumber].handle === state.handle) delete polls[orderNumber]; state.resolve(result); } function terminal(data, response, status) { var success = !!(data && data.paid) || !!SUCCESS_STATUSES[status]; invalidate(options.tags || ["vip", "orders"]); callback(options, "onTerminal", { terminal: true, success: success, status: status, data: data || {}, response: response || null, }); finish({ terminal: true, success: success, status: status, data: data || {}, response: response || null, }); } function schedule(delay) { if (state.done || state.aborted) return; if (document.hidden) { state.waitingForVisible = true; return; } state.waitingForVisible = false; state.timer = window.setTimeout(run, Math.max(0, delay || 0)); } function run() { state.timer = null; if (state.done || state.aborted) return; if (document.hidden) { state.waitingForVisible = true; return; } if (state.attempt >= maxAttempts || Date.now() - state.startedAt >= maxElapsed) { callback(options, "onTimeout", { terminal: false, timedOut: true, orderNumber: orderNumber }); finish({ terminal: false, timedOut: true, status: "timeout" }); return; } state.attempt += 1; state.controller = window.AbortController ? new window.AbortController() : null; var requestOptions = {}; if (state.controller) requestOptions.signal = state.controller.signal; var remaining = Math.max(1000, maxElapsed - (Date.now() - state.startedAt)); state.requestTimer = window.setTimeout(function () { if (state.controller) { try { state.controller.abort(); } catch (error) {} } }, Math.min(20000, remaining)); request(ORDER_PATH + encodeURIComponent(orderNumber), requestOptions).then(function (response) { if (state.requestTimer) { window.clearTimeout(state.requestTimer); state.requestTimer = null; } state.controller = null; if (state.done || state.aborted) return; var data = response.data || response.json || {}; var status = String(data.status || (data.paid ? "paid" : "")).toLowerCase(); callback(options, "onStatus", { status: status, data: data, response: response }); if (response.ok && isTerminal(status, data)) { terminal(data, response, status || "paid"); return; } if (!response.ok && [401, 403, 404].indexOf(response.status) >= 0) { terminal(data, response, "error"); return; } if (state.attempt >= maxAttempts || Date.now() - state.startedAt >= maxElapsed) { callback(options, "onTimeout", { terminal: false, timedOut: true, orderNumber: orderNumber }); finish({ terminal: false, timedOut: true, status: "timeout", response: response }); return; } schedule(backoff(state.attempt)); }); } state.onVisibility = function () { if (document.hidden || state.done || state.aborted) return; if (state.waitingForVisible) { schedule(0); } }; document.addEventListener("visibilitychange", state.onVisibility); state.handle = { promise: state.promise, abort: function () { if (state.done) return; state.aborted = true; finish({ terminal: false, aborted: true, status: "aborted" }); }, }; polls[orderNumber] = state; schedule(0); return state.handle; } function start(options) { options = Object.assign({}, options || {}); var activePoll = null; var stopped = false; var controller = window.AbortController ? new window.AbortController() : null; if (controller) options.signal = controller.signal; var promise = purchase(options).then(function (response) { if (stopped) return { ok: false, aborted: true, status: "aborted" }; var data = response.data || response.json || {}; if (!response.ok) { callback(options, "onError", response); return response; } if (data.redirect && (data.payment_url || data.qr_content)) { callback(options, "onPending", data); if (!data.order_number) { var missing = { ok: false, status: 500, json: { message: "支付订单号缺失" } }; callback(options, "onError", missing); return missing; } activePoll = poll(data.order_number, options); activePoll.promise.then(function (result) { if (result.success) callback(options, "onSuccess", result); else if (result.terminal) callback(options, "onFailure", result); }); return { ok: true, pending: true, data: data, poll: activePoll, }; } invalidate(options.tags || ["vip", "orders"]); callback(options, "onSuccess", { terminal: true, success: true, data: data, response: response }); return { ok: true, pending: false, data: data, response: response }; }); return { promise: promise, abort: function () { stopped = true; if (controller) { try { controller.abort(); } catch (error) {} } if (activePoll && typeof activePoll.abort === "function") activePoll.abort(); }, }; } window.AksrtVipPurchaseCoordinator = { purchase: purchase, exchange: exchange, poll: poll, start: start, abort: function (orderNumber) { var active = polls[String(orderNumber || "")]; if (active && active.handle) active.handle.abort(); }, }; })(window, document);