import { getDomainParts, setDomainParts } from '@/config/domainState' import { DOMAIN_CACHE_KEY, DOMAIN_CACHE_TTL, DOMAIN_CONFIG_URL, DOMAIN_INIT_TIMEOUT, DOMAIN_PROBE_PATH, DOMAIN_PROBE_TIMEOUT, DOMAIN_CACHE_PROBE_TIMEOUT, } from '@/config/domainConstants' let resolving = null let domainReady = false let domainReadyPromise = null let domainReadyResolve = null let initTimeoutId = null function createDomainReadyPromise() { domainReadyPromise = new Promise((resolve) => { domainReadyResolve = resolve }) } function markDomainReady() { if (domainReady) return domainReady = true if (initTimeoutId) { clearTimeout(initTimeoutId) initTimeoutId = null } domainReadyResolve?.() } function notifyDomainChanged(cache) { try { uni.$emit('domain-changed', { ...getDomainParts(), activeSecureUrl: cache?.activeSecureUrl || '', }) } 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() { const cache = readCache() return { ready: domainReady, parts: getDomainParts(), cache, cacheValid: isCacheValid(cache), } } /** 从 secure 完整 URL 解析 ho / dt / ht */ export function parseSecureUrl(url) { if (!url || typeof url !== 'string') { throw new Error('无效的域名地址') } const normalized = url.startsWith('http') ? url : `https://${url}` const matched = normalized.match(/^(https?):\/\/([^/]+)/i) if (!matched) { throw new Error('无法解析域名地址') } const ht = `${matched[1]}:` const hostname = matched[2] const parts = hostname.split('.') if (parts.length < 3) { throw new Error(`域名格式不正确: ${hostname}`) } const dt = parts[parts.length - 1] const ho = parts[parts.length - 2] return { ho, dt, ht, secureUrl: `${ht}//${hostname}` } } export function readCache() { try { const raw = null if (!raw) return null return typeof raw === 'string' ? JSON.parse(raw) : raw } catch (e) { console.warn('[dynamicDomain] 读取缓存失败', e) return null } } function writeCache(payload) { uni.setStorageSync(DOMAIN_CACHE_KEY, payload) } export function isCacheValid(cache) { if (!cache?.ho || !cache?.dt || !cache?.expiresAt) return false return Date.now() < cache.expiresAt } function applyCacheToState(cache) { if (!cache?.ho || !cache?.dt) return false setDomainParts({ ho: cache.ho, dt: cache.dt, ht: cache.ht || 'https:' }) return true } 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 pickActiveSecureUrl(remoteConfig) { const candidates = [] if (remoteConfig?.primary) { candidates.push({ url: remoteConfig.primary, priority: 0 }) } ;(remoteConfig?.backup || []).forEach((url, index) => { if (url) candidates.push({ url, priority: index + 1 }) }) if (!candidates.length) { throw new Error('远程域名配置为空') } const checks = await Promise.all( candidates.map(async (item) => ({ ...item, ok: await probeSecureUrl(item.url), })) ) const winner = checks .filter((item) => item.ok) .sort((a, b) => a.priority - b.priority)[0] if (!winner) { throw new Error('所有域名均不可用') } return winner.url } function applySecureUrl(secureUrl, meta = {}) { const parsed = parseSecureUrl(secureUrl) setDomainParts(parsed) const cache = { ho: parsed.ho, dt: parsed.dt, ht: parsed.ht, activeSecureUrl: parsed.secureUrl, version: meta.version || 1, cachedAt: Date.now(), expiresAt: Date.now() + DOMAIN_CACHE_TTL, } writeCache(cache) notifyDomainChanged(cache) console.log('[dynamicDomain] 当前生效域名', cache) return cache } async function fetchRemoteConfig() { const data = await requestJson(`${DOMAIN_CONFIG_URL}?_t=${Date.now()}`) if (!data?.primary) { throw new Error('远程配置缺少 primary 字段') } return data } /** * 解析并应用动态域名 * @param {{ force?: boolean, verifyCache?: boolean }} options */ export async function resolveDynamicDomain(options = {}) { const { force = false, verifyCache = true } = options const cache = readCache() if (!force && isCacheValid(cache)) { applyCacheToState(cache) if (verifyCache && cache.activeSecureUrl) { const ok = await probeSecureUrl(cache.activeSecureUrl, DOMAIN_CACHE_PROBE_TIMEOUT) if (ok) { console.log('[dynamicDomain] 缓存域名可用,跳过刷新') return cache } console.warn('[dynamicDomain] 缓存域名不可用,重新探测') } else if (!verifyCache) { return cache } } else if (cache?.ho && cache?.dt) { // 缓存过期:先用旧域名,再后台刷新 applyCacheToState(cache) } if (resolving) return resolving resolving = (async () => { try { const remoteConfig = await fetchRemoteConfig() const activeSecureUrl = await pickActiveSecureUrl(remoteConfig) return applySecureUrl(activeSecureUrl, { version: remoteConfig.version }) } catch (error) { if (cache?.ho && cache?.dt) { applyCacheToState(cache) console.warn('[dynamicDomain] 刷新失败,继续使用缓存/默认域名', error) return cache } console.warn('[dynamicDomain] 刷新失败,使用内置默认域名', error) throw error } 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 调用),避免页面请求早于 onLaunch * 探测/切换完成后再放行业务请求,避免缓存域名失效时先发错误请求 */ export function initDynamicDomain() { // #ifndef APP-PLUS return Promise.resolve(null) // #endif if (!domainReadyPromise) createDomainReadyPromise() const cache = readCache() scheduleInitTimeout() const task = isCacheValid(cache) ? resolveDynamicDomain({ force: false, verifyCache: true }) : resolveDynamicDomain({ force: true, verifyCache: false }) return task .catch((e) => { console.warn('[dynamicDomain] 初始化失败,使用默认/过期缓存域名', e) if (cache?.ho && cache?.dt) applyCacheToState(cache) return cache }) .finally(() => { markDomainReady() console.log('[dynamicDomain] 域名就绪', buildActiveSecureUrl()) }) } function buildActiveSecureUrl() { const { ht, ho, dt } = getDomainParts() return `${ht}//secure.${ho}.${dt}` } /** App 再次进入前台:仅缓存过期时后台刷新 */ export function refreshDynamicDomainIfExpired() { // #ifndef APP-PLUS return Promise.resolve(null) // #endif const cache = readCache() if (isCacheValid(cache)) { return Promise.resolve(cache) } return resolveDynamicDomain({ force: true, verifyCache: false }) }