import { getDomainParts, setDomainParts, getDefaultRootDomain, DEFAULT_DOMAIN, } from '@/config/domainState' import { DOMAIN_CONFIG_URL, DOMAIN_INIT_TIMEOUT, DOMAIN_PROBE_PATH, DOMAIN_PROBE_TIMEOUT, DOMAIN_HEARTBEAT_INTERVAL, } from '@/config/domainConstants' let resolving = null let domainReady = false let domainReadyPromise = null let domainReadyResolve = null let initTimeoutId = null let heartbeatTimer = null function createDomainReadyPromise() { domainReadyPromise = new Promise((resolve) => { domainReadyResolve = resolve }) } function markDomainReady() { if (domainReady) return domainReady = true if (initTimeoutId) { clearTimeout(initTimeoutId) initTimeoutId = null } domainReadyResolve?.() } function buildActiveSecureUrl() { const { ht, ho, dt } = getDomainParts() return `${ht}//secure.${ho}.${dt}` } function notifyDomainChanged(parts) { try { uni.$emit('domain-changed', { ...parts, activeSecureUrl: parts.secureUrl || buildActiveSecureUrl(), }) } catch (e) { // ignore } } /** 业务请求需等待域名就绪(仅 APP) */ export function whenDomainReady() { // #ifndef APP-PLUS return Promise.resolve() // #endif if (domainReady) return Promise.resolve() if (!domainReadyPromise) createDomainReadyPromise() return domainReadyPromise } /** 当前域名状态(调试用) */ export function getDomainStatus() { return { ready: domainReady, parts: getDomainParts(), activeSecureUrl: buildActiveSecureUrl(), defaultRoot: getDefaultRootDomain(), defaultSecureUrl: `https://secure.${DEFAULT_DOMAIN.ho}.${DEFAULT_DOMAIN.dt}`, } } /** * 解析根域名配置项 * 支持:cwgbroker.club / secure.cwgbroker.club / https://secure.cwgbroker.club */ export function parseRootDomain(input) { if (!input || typeof input !== 'string') { throw new Error('无效的域名') } let host = input.trim() host = host.replace(/^https?:\/\//i, '') host = host.split('/')[0] host = host.replace(/^secure\./i, '') const parts = host.split('.') if (parts.length < 2) { throw new Error(`域名格式不正确: ${input}`) } const dt = parts[parts.length - 1] const ho = parts[parts.length - 2] const ht = 'https:' return { ho, dt, ht, root: `${ho}.${dt}`, secureUrl: `${ht}//secure.${ho}.${dt}`, } } /** @deprecated 兼容旧调用 */ export function parseSecureUrl(url) { return parseRootDomain(url) } function parseResponseData(data) { if (!data) return null if (typeof data === 'string') { try { return JSON.parse(data) } catch (e) { return null } } return data } function requestJson(url, timeout = 8000) { return new Promise((resolve, reject) => { uni.request({ url, method: 'GET', timeout, success: (res) => { if (res.statusCode !== 200) { reject(new Error(`请求失败: ${res.statusCode}`)) return } const body = parseResponseData(res.data) if (!body) { reject(new Error('配置解析失败')) return } resolve(body) }, fail: (err) => reject(err || new Error('网络请求失败')), }) }) } /** 探测 secure 子域是否可用 */ export function probeSecureUrl(secureUrl, timeout = DOMAIN_PROBE_TIMEOUT) { const url = `${secureUrl.replace(/\/$/, '')}${DOMAIN_PROBE_PATH}?_t=${Date.now()}` return new Promise((resolve) => { uni.request({ url, method: 'GET', timeout, success: (res) => resolve(res.statusCode === 200), fail: () => resolve(false), }) }) } async function pickActiveRootDomain(remoteConfig) { const candidates = [] if (remoteConfig?.primary) { candidates.push({ root: remoteConfig.primary, priority: 0 }) } ;(remoteConfig?.backup || []).forEach((root, index) => { if (root) candidates.push({ root, priority: index + 1 }) }) if (!candidates.length) { throw new Error('远程域名配置为空') } const checks = await Promise.all( candidates.map(async (item) => { const parsed = parseRootDomain(item.root) return { ...item, parsed, ok: await probeSecureUrl(parsed.secureUrl), } }) ) const winner = checks .filter((item) => item.ok) .sort((a, b) => a.priority - b.priority)[0] if (!winner) { console.warn('[dynamicDomain] primary/backup 均不可用,回退代码默认域名') return getDefaultRootDomain() } return winner.root } function applyRootDomain(root, meta = {}) { const parsed = parseRootDomain(root) const before = getDomainParts() setDomainParts(parsed) const changed = before.ho !== parsed.ho || before.dt !== parsed.dt if (changed) { notifyDomainChanged(parsed) console.log('[dynamicDomain] 域名已切换', { from: `${before.ho}.${before.dt}`, to: parsed.root, secureUrl: parsed.secureUrl, version: meta.version, fallback: meta.fallback || false, }) } else { console.log('[dynamicDomain] 域名可用', parsed.secureUrl) } return parsed } /** primary/backup 均不可用或拉配置失败时,回退代码内置默认域名 */ function applyDefaultDomain(meta = {}) { return applyRootDomain(getDefaultRootDomain(), { ...meta, fallback: true }) } async function fetchRemoteConfig() { const data = await requestJson(`${DOMAIN_CONFIG_URL}?_t=${Date.now()}`) if (!data?.primary) { throw new Error('远程配置缺少 primary 字段') } return data } /** 拉取配置并探测可用根域名 */ export async function resolveDynamicDomain() { if (resolving) return resolving resolving = (async () => { try { const remoteConfig = await fetchRemoteConfig() const activeRoot = await pickActiveRootDomain(remoteConfig) return applyRootDomain(activeRoot, { version: remoteConfig.version }) } catch (error) { console.warn('[dynamicDomain] 解析失败,回退代码默认域名', error) return applyDefaultDomain() } finally { resolving = null } })() return resolving } function scheduleInitTimeout() { if (initTimeoutId) return initTimeoutId = setTimeout(() => { console.warn(`[dynamicDomain] 初始化超过 ${DOMAIN_INIT_TIMEOUT}ms,放行业务请求`) markDomainReady() }, DOMAIN_INIT_TIMEOUT) } /** 启动时解析域名(main.js 调用) */ export function initDynamicDomain() { // #ifndef APP-PLUS return Promise.resolve(null) // #endif if (!domainReadyPromise) createDomainReadyPromise() try { uni.removeStorageSync('app-dynamic-domain') } catch (e) { // ignore } scheduleInitTimeout() return resolveDynamicDomain() .finally(() => { markDomainReady() startDomainHeartbeat() console.log('[dynamicDomain] 域名就绪', buildActiveSecureUrl()) }) } /** 每次 App 进入前台时重新检测 */ export function refreshDynamicDomainOnShow() { // #ifndef APP-PLUS return Promise.resolve(null) // #endif if (!domainReady) { return whenDomainReady() } return resolveDynamicDomain().catch((e) => { console.warn('[dynamicDomain] 前台刷新失败', e) }) } /** App 运行期间定时心跳检测(10 分钟) */ export function startDomainHeartbeat() { // #ifndef APP-PLUS return // #endif stopDomainHeartbeat() heartbeatTimer = setInterval(() => { console.log('[dynamicDomain] 心跳检测') resolveDynamicDomain().catch((e) => { console.warn('[dynamicDomain] 心跳检测失败', e) }) }, DOMAIN_HEARTBEAT_INTERVAL) } export function stopDomainHeartbeat() { if (heartbeatTimer) { clearInterval(heartbeatTimer) heartbeatTimer = null } }