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