zhb пре 6 дана
родитељ
комит
cb131fb465
5 измењених фајлова са 161 додато и 169 уклоњено
  1. 6 4
      App.vue
  2. 8 8
      config/domainConstants.ts
  3. 20 3
      config/domainState.ts
  4. 0 21
      config/index.ts
  5. 127 133
      utils/dynamicDomain.js

+ 6 - 4
App.vue

@@ -19,7 +19,8 @@
   import { useAppUpdate } from '@/hooks/useAppUpdate'
   import {
     whenDomainReady,
-    refreshDynamicDomainIfExpired,
+    refreshDynamicDomainOnShow,
+    stopDomainHeartbeat,
   } from '@/utils/dynamicDomain'
 
   const { checkUpdate } = useAppUpdate()
@@ -36,9 +37,7 @@
     handleSignupRoute(options)
 
     // #ifdef APP-PLUS
-    refreshDynamicDomainIfExpired().catch((e) => {
-      console.warn('[dynamicDomain] 前台刷新失败', e)
-    })
+    refreshDynamicDomainOnShow()
     // #endif
   })
 
@@ -428,6 +427,9 @@
     // #endif
   })
   onUnmounted(() => {
+    // #ifdef APP-PLUS
+    stopDomainHeartbeat()
+    // #endif
     // #ifdef H5
     window.removeEventListener('hashchange', handleSignupRoute)
     if (windowWidth.value < 700) return

+ 8 - 8
config/domainConstants.ts

@@ -1,12 +1,12 @@
-/** 远程域名配置(Gogs raw) */
+/** 远程域名配置(根域名列表:primary + backup) */
 export const DOMAIN_CONFIG_URL =
-  'http://112.213.107.185:3000/zhb/app-dynamic-domain/raw/master/config.json'
+  'https://www.cwgvu-git.com/cwg/dynamic-domain/raw/master/config.json'
 
-export const DOMAIN_CACHE_KEY = 'app-dynamic-domain'
-export const DOMAIN_CACHE_TTL = 24 * 60 * 60 * 1000
-/** 首次无缓存时,超过该时间仍放行业务请求,避免一直卡住 */
+/** 首次启动解析超时后仍放行业务请求 */
 export const DOMAIN_INIT_TIMEOUT = 12000
+/** 探测单个域名超时 */
 export const DOMAIN_PROBE_TIMEOUT = 5000
-/** 启动时校验缓存域名,超时更短以尽快切换 */
-export const DOMAIN_CACHE_PROBE_TIMEOUT = 3000
-export const DOMAIN_PROBE_PATH = '/wgt/list.json'
+/** 可用性探测路径 */
+export const DOMAIN_PROBE_PATH = '/custom/ping'
+/** App 前台运行期间心跳检测间隔 */
+export const DOMAIN_HEARTBEAT_INTERVAL = 10 * 60 * 1000

+ 20 - 3
config/domainState.ts

@@ -1,7 +1,18 @@
+/** 代码内置默认根域名(远程 primary/backup 均不可用时回退) */
+export const DEFAULT_DOMAIN = {
+  ht: 'https:',
+  ho: 'cwgbroker',
+  dt: 'club',
+} as const
+
+export function getDefaultRootDomain() {
+  return `${DEFAULT_DOMAIN.ho}.${DEFAULT_DOMAIN.dt}`
+}
+
 /** APP 动态域名运行时状态(可被 dynamicDomain 更新) */
-let ht = 'https:'
-let ho = 'cwgbroker'
-let dt = 'club'
+let ht: string = DEFAULT_DOMAIN.ht
+let ho: string = DEFAULT_DOMAIN.ho
+let dt: string = DEFAULT_DOMAIN.dt
 
 export function getDomainParts() {
   return { ht, ho, dt }
@@ -13,6 +24,12 @@ export function setDomainParts(parts: { ho: string; dt: string; ht?: string }) {
   if (parts.ht) ht = parts.ht
 }
 
+export function resetToDefaultDomain() {
+  ht = DEFAULT_DOMAIN.ht
+  ho = DEFAULT_DOMAIN.ho
+  dt = DEFAULT_DOMAIN.dt
+}
+
 export function buildHostUrls() {
   return {
     HostWs: `wss://ws.${ho}.${dt}`,

+ 0 - 21
config/index.ts

@@ -1,21 +1,4 @@
 import { getDomainParts, setDomainParts, buildHostUrls } from './domainState'
-import { DOMAIN_CACHE_KEY } from './domainConstants'
-
-declare const uni: any
-
-/** 模块加载时同步恢复缓存(含过期缓存),避免首屏走默认域名 */
-function loadCachedDomainSync() {
-  try {
-    const raw = uni.getStorageSync(DOMAIN_CACHE_KEY)
-    if (!raw) return
-    const cache = typeof raw === 'string' ? JSON.parse(raw) : raw
-    if (cache?.ho && cache?.dt) {
-      setDomainParts({ ho: cache.ho, dt: cache.dt, ht: cache.ht || 'https:' })
-    }
-  } catch (e) {
-    console.warn('[dynamicDomain] 同步恢复缓存失败', e)
-  }
-}
 
 // #ifdef H5
 const [p, h] = [window.location.protocol, window.location.host]
@@ -25,10 +8,6 @@ const h5Ht = p == 'http:' ? 'https:' : p
 setDomainParts({ ho: h5Ho, dt: h5Dt, ht: h5Ht })
 // #endif
 
-// #ifdef APP-PLUS
-loadCachedDomainSync()
-// #endif
-
 const staticConfig = {
   Code: {
     StatusOK: 200,

+ 127 - 133
utils/dynamicDomain.js

@@ -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 })
 }