|
|
@@ -1,12 +1,15 @@
|
|
|
-import { getDomainParts, setDomainParts } from '@/config/domainState'
|
|
|
import {
|
|
|
- DOMAIN_CACHE_KEY,
|
|
|
- DOMAIN_CACHE_TTL,
|
|
|
+ getDomainParts,
|
|
|
+ setDomainParts,
|
|
|
+ getDefaultRootDomain,
|
|
|
+ DEFAULT_DOMAIN,
|
|
|
+} from '@/config/domainState'
|
|
|
+import {
|
|
|
DOMAIN_CONFIG_URL,
|
|
|
DOMAIN_INIT_TIMEOUT,
|
|
|
DOMAIN_PROBE_PATH,
|
|
|
DOMAIN_PROBE_TIMEOUT,
|
|
|
- DOMAIN_CACHE_PROBE_TIMEOUT,
|
|
|
+ DOMAIN_HEARTBEAT_INTERVAL,
|
|
|
} from '@/config/domainConstants'
|
|
|
|
|
|
let resolving = null
|
|
|
@@ -14,6 +17,7 @@ let domainReady = false
|
|
|
let domainReadyPromise = null
|
|
|
let domainReadyResolve = null
|
|
|
let initTimeoutId = null
|
|
|
+let heartbeatTimer = null
|
|
|
|
|
|
function createDomainReadyPromise() {
|
|
|
domainReadyPromise = new Promise((resolve) => {
|
|
|
@@ -31,11 +35,16 @@ function markDomainReady() {
|
|
|
domainReadyResolve?.()
|
|
|
}
|
|
|
|
|
|
-function notifyDomainChanged(cache) {
|
|
|
+function buildActiveSecureUrl() {
|
|
|
+ const { ht, ho, dt } = getDomainParts()
|
|
|
+ return `${ht}//secure.${ho}.${dt}`
|
|
|
+}
|
|
|
+
|
|
|
+function notifyDomainChanged(parts) {
|
|
|
try {
|
|
|
uni.$emit('domain-changed', {
|
|
|
- ...getDomainParts(),
|
|
|
- activeSecureUrl: cache?.activeSecureUrl || '',
|
|
|
+ ...parts,
|
|
|
+ activeSecureUrl: parts.secureUrl || buildActiveSecureUrl(),
|
|
|
})
|
|
|
} catch (e) {
|
|
|
// ignore
|
|
|
@@ -54,60 +63,49 @@ export function whenDomainReady() {
|
|
|
|
|
|
/** 当前域名状态(调试用) */
|
|
|
export function getDomainStatus() {
|
|
|
- const cache = readCache()
|
|
|
return {
|
|
|
ready: domainReady,
|
|
|
parts: getDomainParts(),
|
|
|
- cache,
|
|
|
- cacheValid: isCacheValid(cache),
|
|
|
+ activeSecureUrl: buildActiveSecureUrl(),
|
|
|
+ defaultRoot: getDefaultRootDomain(),
|
|
|
+ defaultSecureUrl: `https://secure.${DEFAULT_DOMAIN.ho}.${DEFAULT_DOMAIN.dt}`,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** 从 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('无法解析域名地址')
|
|
|
+/**
|
|
|
+ * 解析根域名配置项
|
|
|
+ * 支持:cwgbroker.club / secure.cwgbroker.club / https://secure.cwgbroker.club
|
|
|
+ */
|
|
|
+export function parseRootDomain(input) {
|
|
|
+ if (!input || typeof input !== 'string') {
|
|
|
+ throw new Error('无效的域名')
|
|
|
}
|
|
|
- const ht = `${matched[1]}:`
|
|
|
- const hostname = matched[2]
|
|
|
- const parts = hostname.split('.')
|
|
|
- if (parts.length < 3) {
|
|
|
- throw new Error(`域名格式不正确: ${hostname}`)
|
|
|
+ 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]
|
|
|
- return { ho, dt, ht, secureUrl: `${ht}//${hostname}` }
|
|
|
-}
|
|
|
+ const ht = 'https:'
|
|
|
|
|
|
-export function readCache() {
|
|
|
- try {
|
|
|
- const raw = uni.getStorageSync(DOMAIN_CACHE_KEY)
|
|
|
- if (!raw) return null
|
|
|
- return typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
|
- } catch (e) {
|
|
|
- console.warn('[dynamicDomain] 读取缓存失败', e)
|
|
|
- return null
|
|
|
+ return {
|
|
|
+ ho,
|
|
|
+ dt,
|
|
|
+ ht,
|
|
|
+ root: `${ho}.${dt}`,
|
|
|
+ secureUrl: `${ht}//secure.${ho}.${dt}`,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-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
|
|
|
+/** @deprecated 兼容旧调用 */
|
|
|
+export function parseSecureUrl(url) {
|
|
|
+ return parseRootDomain(url)
|
|
|
}
|
|
|
|
|
|
function parseResponseData(data) {
|
|
|
@@ -145,7 +143,7 @@ function requestJson(url, timeout = 8000) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-/** 快速探测 secure 域名是否可用 */
|
|
|
+/** 探测 secure 子域是否可用 */
|
|
|
export function probeSecureUrl(secureUrl, timeout = DOMAIN_PROBE_TIMEOUT) {
|
|
|
const url = `${secureUrl.replace(/\/$/, '')}${DOMAIN_PROBE_PATH}?_t=${Date.now()}`
|
|
|
return new Promise((resolve) => {
|
|
|
@@ -159,13 +157,13 @@ export function probeSecureUrl(secureUrl, timeout = DOMAIN_PROBE_TIMEOUT) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-async function pickActiveSecureUrl(remoteConfig) {
|
|
|
+async function pickActiveRootDomain(remoteConfig) {
|
|
|
const candidates = []
|
|
|
if (remoteConfig?.primary) {
|
|
|
- candidates.push({ url: remoteConfig.primary, priority: 0 })
|
|
|
+ candidates.push({ root: remoteConfig.primary, priority: 0 })
|
|
|
}
|
|
|
- ;(remoteConfig?.backup || []).forEach((url, index) => {
|
|
|
- if (url) candidates.push({ url, priority: index + 1 })
|
|
|
+ ;(remoteConfig?.backup || []).forEach((root, index) => {
|
|
|
+ if (root) candidates.push({ root, priority: index + 1 })
|
|
|
})
|
|
|
|
|
|
if (!candidates.length) {
|
|
|
@@ -173,39 +171,52 @@ async function pickActiveSecureUrl(remoteConfig) {
|
|
|
}
|
|
|
|
|
|
const checks = await Promise.all(
|
|
|
- candidates.map(async (item) => ({
|
|
|
- ...item,
|
|
|
- ok: await probeSecureUrl(item.url),
|
|
|
- }))
|
|
|
+ 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) {
|
|
|
- throw new Error('所有域名均不可用')
|
|
|
+ console.warn('[dynamicDomain] primary/backup 均不可用,回退代码默认域名')
|
|
|
+ return getDefaultRootDomain()
|
|
|
}
|
|
|
|
|
|
- return winner.url
|
|
|
+ return winner.root
|
|
|
}
|
|
|
|
|
|
-function applySecureUrl(secureUrl, meta = {}) {
|
|
|
- const parsed = parseSecureUrl(secureUrl)
|
|
|
+function applyRootDomain(root, meta = {}) {
|
|
|
+ const parsed = parseRootDomain(root)
|
|
|
+ const before = getDomainParts()
|
|
|
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,
|
|
|
+
|
|
|
+ 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)
|
|
|
}
|
|
|
- writeCache(cache)
|
|
|
- notifyDomainChanged(cache)
|
|
|
- console.log('[dynamicDomain] 当前生效域名', cache)
|
|
|
- return cache
|
|
|
+
|
|
|
+ return parsed
|
|
|
+}
|
|
|
+
|
|
|
+/** primary/backup 均不可用或拉配置失败时,回退代码内置默认域名 */
|
|
|
+function applyDefaultDomain(meta = {}) {
|
|
|
+ return applyRootDomain(getDefaultRootDomain(), { ...meta, fallback: true })
|
|
|
}
|
|
|
|
|
|
async function fetchRemoteConfig() {
|
|
|
@@ -216,46 +227,18 @@ async function fetchRemoteConfig() {
|
|
|
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)
|
|
|
- }
|
|
|
-
|
|
|
+/** 拉取配置并探测可用根域名 */
|
|
|
+export async function resolveDynamicDomain() {
|
|
|
if (resolving) return resolving
|
|
|
|
|
|
resolving = (async () => {
|
|
|
try {
|
|
|
const remoteConfig = await fetchRemoteConfig()
|
|
|
- const activeSecureUrl = await pickActiveSecureUrl(remoteConfig)
|
|
|
- return applySecureUrl(activeSecureUrl, { version: remoteConfig.version })
|
|
|
+ const activeRoot = await pickActiveRootDomain(remoteConfig)
|
|
|
+ return applyRootDomain(activeRoot, { version: remoteConfig.version })
|
|
|
} catch (error) {
|
|
|
- if (cache?.ho && cache?.dt) {
|
|
|
- applyCacheToState(cache)
|
|
|
- console.warn('[dynamicDomain] 刷新失败,继续使用缓存/默认域名', error)
|
|
|
- return cache
|
|
|
- }
|
|
|
- console.warn('[dynamicDomain] 刷新失败,使用内置默认域名', error)
|
|
|
- throw error
|
|
|
+ console.warn('[dynamicDomain] 解析失败,回退代码默认域名', error)
|
|
|
+ return applyDefaultDomain()
|
|
|
} finally {
|
|
|
resolving = null
|
|
|
}
|
|
|
@@ -272,50 +255,61 @@ function scheduleInitTimeout() {
|
|
|
}, DOMAIN_INIT_TIMEOUT)
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 尽早初始化(main.js 调用),避免页面请求早于 onLaunch
|
|
|
- * 探测/切换完成后再放行业务请求,避免缓存域名失效时先发错误请求
|
|
|
- */
|
|
|
+/** 启动时解析域名(main.js 调用) */
|
|
|
export function initDynamicDomain() {
|
|
|
// #ifndef APP-PLUS
|
|
|
return Promise.resolve(null)
|
|
|
// #endif
|
|
|
|
|
|
if (!domainReadyPromise) createDomainReadyPromise()
|
|
|
-
|
|
|
- const cache = readCache()
|
|
|
+ try {
|
|
|
+ uni.removeStorageSync('app-dynamic-domain')
|
|
|
+ } catch (e) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
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
|
|
|
- })
|
|
|
+ return resolveDynamicDomain()
|
|
|
.finally(() => {
|
|
|
markDomainReady()
|
|
|
+ startDomainHeartbeat()
|
|
|
console.log('[dynamicDomain] 域名就绪', buildActiveSecureUrl())
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function buildActiveSecureUrl() {
|
|
|
- const { ht, ho, dt } = getDomainParts()
|
|
|
- return `${ht}//secure.${ho}.${dt}`
|
|
|
+/** 每次 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 再次进入前台:仅缓存过期时后台刷新 */
|
|
|
-export function refreshDynamicDomainIfExpired() {
|
|
|
+/** App 运行期间定时心跳检测(10 分钟) */
|
|
|
+export function startDomainHeartbeat() {
|
|
|
// #ifndef APP-PLUS
|
|
|
- return Promise.resolve(null)
|
|
|
+ return
|
|
|
// #endif
|
|
|
|
|
|
- const cache = readCache()
|
|
|
- if (isCacheValid(cache)) {
|
|
|
- return Promise.resolve(cache)
|
|
|
+ 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
|
|
|
}
|
|
|
- return resolveDynamicDomain({ force: true, verifyCache: false })
|
|
|
}
|