|
@@ -0,0 +1,321 @@
|
|
|
|
|
+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 })
|
|
|
|
|
+}
|