/** * 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 operationKeys = Object.create(null); var polls = Object.create(null); var lastScope = ""; var scopeGeneration = 0; 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; var current = userId > 0 ? "user:" + userId : (config.isLoggedIn ? "auth" : "guest"); if (lastScope && lastScope !== current) clearFlights(); lastScope = current; return current; } function clearFlights() { scopeGeneration += 1; Object.keys(createFlights).forEach(function (key) { var flight = createFlights[key]; if (flight && typeof flight.cancel === "function") flight.cancel(); }); createFlights = Object.create(null); operationKeys = Object.create(null); Object.keys(polls).forEach(function (pollKey) { var active = polls[pollKey]; 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; if (options.scope && userScope() !== options.scope) return; if (options.generation !== undefined && scopeGeneration !== options.generation) return; if (typeof options.isActive === "function" && !options.isActive()) 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].promise; if (!operationKeys[key]) operationKeys[key] = window.AksrtRequest.createIdempotencyKey(); var requestController = !options.signal && window.AbortController ? new window.AbortController() : null; var flight = { promise: request("/pay/vip", { method: "POST", headers: { "Idempotency-Key": operationKeys[key] }, body: JSON.stringify({ package_id: packageId, payment_method: method, }), signal: options.signal || (requestController ? requestController.signal : undefined), }), cancel: function () { if (typeof options.cancel === "function") { try { options.cancel(); } catch (error) {} } if (requestController) { try { requestController.abort(); } catch (error) {} } }, }; createFlights[key] = flight; flight.promise.then(function () { if (createFlights[key] === flight) delete createFlights[key]; }, function () { if (createFlights[key] === flight) delete createFlights[key]; }); flight.promise.then(function (response) { if (response.ok) delete operationKeys[key]; }); return flight.promise; } function exchange(options) { options = options || {}; var scope = userScope(); var packageId = parseInt(options.packageId || options.package_id || 0, 10); if (!packageId) { return Promise.resolve({ ok: false, status: 400, json: { message: "兑换套餐无效" } }); } var key = scope + ":points-exchange:" + String(packageId); if (!operationKeys[key]) operationKeys[key] = window.AksrtRequest.createIdempotencyKey(); return request("/pay/vip/points-exchange", { method: "POST", headers: { "Idempotency-Key": operationKeys[key] }, body: JSON.stringify({ package_id: packageId }), signal: options.signal, }).then(function (response) { if (response.ok) { delete operationKeys[key]; 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 = Object.assign({}, options || {}); orderNumber = String(orderNumber || ""); if (!orderNumber) { return { promise: Promise.resolve({ terminal: true, success: false, status: "invalid" }), abort: function () {}, }; } var pollScope = userScope(); var pollGeneration = scopeGeneration; if (!options.scope) options.scope = pollScope; if (options.generation === undefined) options.generation = pollGeneration; var pollKey = pollScope + ":" + orderNumber; if (polls[pollKey]) return polls[pollKey].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[pollKey] && polls[pollKey].handle === state.handle) delete polls[pollKey]; state.resolve(result); } function terminal(data, response, status) { if ((options.scope && userScope() !== options.scope) || (options.generation !== undefined && scopeGeneration !== options.generation)) { finish({ terminal: false, aborted: true, status: "scope_changed" }); return; } 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; // Detect account transitions and explicit auth resets even when no event reached this page. if (userScope() !== pollScope || scopeGeneration !== pollGeneration || 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[pollKey] = 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; var startScope = userScope(); var startGeneration = scopeGeneration; if (controller) options.signal = controller.signal; options.scope = startScope; options.generation = startGeneration; options.isActive = function () { return !stopped && scopeGeneration === startGeneration && userScope() === startScope; }; options.cancel = function () { if (controller) { try { controller.abort(); } catch (error) {} } }; var promise = purchase(options).then(function (response) { if (!options.isActive()) { return { ok: false, aborted: true, status: stopped ? "aborted" : "scope_changed" }; } 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; } if (!options.isActive()) { return { ok: false, aborted: true, status: "scope_changed" }; } 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, }; } if (!options.isActive()) { return { ok: false, aborted: true, status: "scope_changed" }; } 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; options.cancel(); if (activePoll && typeof activePoll.abort === "function") activePoll.abort(); }, }; } window.AksrtVipPurchaseCoordinator = { purchase: purchase, exchange: exchange, poll: poll, start: start, abort: function (orderNumber) { var key = userScope() + ":" + String(orderNumber || ""); var active = polls[key]; if (active && active.handle) active.handle.abort(); }, }; })(window, document);