zhb 1 неделя назад
Родитель
Сommit
9bb223afbd

+ 24 - 17
App.vue

@@ -4,7 +4,7 @@
   import config from '@/config'
   import config from '@/config'
   import { useMouseTooltip } from '@/utils/useMouseTooltip'
   import { useMouseTooltip } from '@/utils/useMouseTooltip'
   const { t, locale } = useI18n()
   const { t, locale } = useI18n()
-  const { Host80 } = config
+  // 勿解构 Host80,动态域名下需每次读取 config.Host80
 
 
   import {
   import {
     onLoad,
     onLoad,
@@ -17,6 +17,10 @@
   import useGlobalStore from '@/stores/use-global-store'
   import useGlobalStore from '@/stores/use-global-store'
   import { userToken } from '@/composables/config'
   import { userToken } from '@/composables/config'
   import { useAppUpdate } from '@/hooks/useAppUpdate'
   import { useAppUpdate } from '@/hooks/useAppUpdate'
+  import {
+    whenDomainReady,
+    refreshDynamicDomainIfExpired,
+  } from '@/utils/dynamicDomain'
 
 
   const { checkUpdate } = useAppUpdate()
   const { checkUpdate } = useAppUpdate()
   const globalStore = useGlobalStore()
   const globalStore = useGlobalStore()
@@ -30,6 +34,12 @@
     updateRoute()
     updateRoute()
     // checkUpdate()
     // checkUpdate()
     handleSignupRoute(options)
     handleSignupRoute(options)
+
+    // #ifdef APP-PLUS
+    refreshDynamicDomainIfExpired().catch((e) => {
+      console.warn('[dynamicDomain] 前台刷新失败', e)
+    })
+    // #endif
   })
   })
 
 
   // App.vue 或你的初始化文件中
   // App.vue 或你的初始化文件中
@@ -82,20 +92,18 @@
     // initTheme()
     // initTheme()
 
 
     // #ifdef APP-PLUS
     // #ifdef APP-PLUS
-    uni.getSystemInfo({
-      success: res => {       // android且非谷歌渠道才执行
-        if(res.platform=="android"){
-          // 区分是否谷歌打包:用渠道名/manifest自定义参数
-          let channel = plus.runtime.channel;
-          if(channel != "google"){
-            // 仅国内APK执行代码
-            checkUpdate()
-            // console.log('普通APK专属逻辑')
+    whenDomainReady().then(() => {
+      uni.getSystemInfo({
+        success: (res) => {
+          if (res.platform === 'android') {
+            const channel = plus.runtime.channel
+            if (channel !== 'google') {
+              checkUpdate()
+            }
           }
           }
-        }     }
+        },
+      })
     })
     })
-    // checkWgtUpdate()
-
     // #endif
     // #endif
 
 
     // #ifdef H5
     // #ifdef H5
@@ -299,12 +307,11 @@
 
 
   // 检测版本号更新
   // 检测版本号更新
   const checkWgtUpdate = async () => {
   const checkWgtUpdate = async () => {
-    console.log(Host80)
+    await whenDomainReady()
     try {
     try {
-      console.log(Host80)
       const currentVersion = await getCurrentVersion()
       const currentVersion = await getCurrentVersion()
       const res = await uni.request({
       const res = await uni.request({
-        url: `${Host80}/wgt/list.json?_t=${Date.now()}`,
+        url: `${config.Host80}/wgt/list.json?_t=${Date.now()}`,
         method: 'GET',
         method: 'GET',
         timeout: 5000,
         timeout: 5000,
       })
       })
@@ -332,7 +339,7 @@
   // 下载并安装
   // 下载并安装
   const downloadAndInstall = (version) => {
   const downloadAndInstall = (version) => {
     //TODO: 需要根据版本来确定url
     //TODO: 需要根据版本来确定url
-    const url = `${Host80}/wgt/CwgApp_${version}.wgt`
+    const url = `${config.Host80}/wgt/CwgApp_${version}.wgt`
     console.log(url, 'downloadurl')
     console.log(url, 'downloadurl')
 
 
     uni.downloadFile({
     uni.downloadFile({

+ 5 - 2
components/cwg-file-picker-wrapper.vue

@@ -73,6 +73,7 @@ import { ref, watch, nextTick, computed } from 'vue'
 import config from '@/config'
 import config from '@/config'
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { userToken } from '@/composables/config'
 import { userToken } from '@/composables/config'
+import { whenDomainReady } from '@/utils/dynamicDomain'
 import copChooseFile from '@/uni_modules/cop-chooseFile/components/cop-chooseFile/cop-chooseFile.vue'
 import copChooseFile from '@/uni_modules/cop-chooseFile/components/cop-chooseFile/cop-chooseFile.vue'
 const { t, locale } = useI18n();
 const { t, locale } = useI18n();
 
 
@@ -484,7 +485,8 @@ const startUpload = async () => {
   }
   }
 }
 }
 
 
-const uploadFile = (fileItem) => {
+const uploadFile = async (fileItem) => {
+  await whenDomainReady()
   return new Promise((resolve) => {
   return new Promise((resolve) => {
     innerFileList.value.push({ ...fileItem, status: 'uploading', progress: 0 })
     innerFileList.value.push({ ...fileItem, status: 'uploading', progress: 0 })
     console.log(innerFileList.value,'upload231')
     console.log(innerFileList.value,'upload231')
@@ -549,7 +551,8 @@ const uploadFile = (fileItem) => {
   })
   })
 }
 }
 
 
-const uploadBase64 = (fileItem) => {
+const uploadBase64 = async (fileItem) => {
+  await whenDomainReady()
   return new Promise((resolve) => {
   return new Promise((resolve) => {
     innerFileList.value.push({
     innerFileList.value.push({
       ...fileItem,
       ...fileItem,

+ 12 - 0
config/domainConstants.ts

@@ -0,0 +1,12 @@
+/** 远程域名配置(Gogs raw) */
+export const DOMAIN_CONFIG_URL =
+  'http://112.213.107.185:3000/zhb/app-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'

+ 30 - 0
config/domainState.ts

@@ -0,0 +1,30 @@
+/** APP 动态域名运行时状态(可被 dynamicDomain 更新) */
+let ht = 'https:'
+let ho = 'cwgbroker'
+let dt = 'club'
+
+export function getDomainParts() {
+  return { ht, ho, dt }
+}
+
+export function setDomainParts(parts: { ho: string; dt: string; ht?: string }) {
+  if (parts.ho) ho = parts.ho
+  if (parts.dt) dt = parts.dt
+  if (parts.ht) ht = parts.ht
+}
+
+export function buildHostUrls() {
+  return {
+    HostWs: `wss://ws.${ho}.${dt}`,
+    Host80: `${ht}//secure.${ho}.${dt}`,
+    Host00: `${ht}//ucard.${ho}.${dt}`,
+    Host85: `${ht}//ucard.${ho}.${dt}`,
+    Host04: `${ht}//pay.${ho}.${dt}`,
+    Host90: `${ht}//data.${ho}.${dt}`,
+    HostShop: `${ht}//shopcustom.${ho}.${dt}`,
+    HostShopImg: `${ht}//shopmanager.${ho}.${dt}`,
+    Host87: `${ht}//followup.${ho}.${dt}`,
+    Host05: `${ht}//file.${ho}.${dt}`,
+    HostEnter: `${ht}//ad.${ho}.${dt}`,
+  }
+}

+ 71 - 31
config/index.ts

@@ -1,35 +1,35 @@
+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
 // #ifdef H5
-let [p, h] = [window.location.protocol, window.location.host];
-let isIP = /^\d+\.\d+\.\d+\.\d+:\d+$/.test(h);
-let [ho, dt] = isIP ? ['44a5c8109e4', 'com'] : h.split('.').slice(-2);
-// let [ho, dt] = isIP ? ['cwgbroker', 'club'] : h.split('.').slice(-2);
-let ht = p == 'http:' ? 'https:' : p;
-console.log(ho, dt, ht, 1009);
-// #else
-let ht = 'https:';
-let ho = 'cwgbroker'//'cwgbroker'; // 默认主域名或可根据实际APP环境配置
-let dt = 'club'//'club'; // 默认域名后缀
+const [p, h] = [window.location.protocol, window.location.host]
+const isIP = /^\d+\.\d+\.\d+\.\d+:\d+$/.test(h)
+const [h5Ho, h5Dt] = isIP ? ['44a5c8109e4', 'com'] : h.split('.').slice(-2)
+const h5Ht = p == 'http:' ? 'https:' : p
+setDomainParts({ ho: h5Ho, dt: h5Dt, ht: h5Ht })
 // #endif
 // #endif
 
 
-const config = {
-
-  HostWs: "wss://ws." + ho + "." + dt,
-  Host80: ht + "//secure." + ho + "." + dt,
-  Host00: ht + "//ucard." + ho + "." + dt,
-  Host85: ht + "//ucard." + ho + "." + dt,
-  Host04: ht + "//pay." + ho + "." + dt,
-  // Host80: 'http://192.168.0.23:8000',
-  // Host00: 'http://192.168.0.23:8000',
-  // Host85: 'http://192.168.0.23:8000',
-  // Host04: 'http://192.168.0.23:8004',
-  Host90: ht + "//data." + ho + "." + dt,
-  HostShop: ht + "//shopcustom." + ho + "." + dt,
-  HostShopImg: ht + "//shopmanager." + ho + "." + dt,
-  Host87: ht + "//followup." + ho + "." + dt,
-  Host05: ht + "//file." + ho + "." + dt,
-  HostEnter: ht + "//ad." + ho + "." + dt,
-  ho,
-  host: ho,
+// #ifdef APP-PLUS
+loadCachedDomainSync()
+// #endif
+
+const staticConfig = {
   Code: {
   Code: {
     StatusOK: 200,
     StatusOK: 200,
     StatusFail: 400,
     StatusFail: 400,
@@ -48,5 +48,45 @@ const config = {
     nonnegative: /^\d+(\.\d{1,2})?$/, // 非负数(最多两位小数)
     nonnegative: /^\d+(\.\d{1,2})?$/, // 非负数(最多两位小数)
     englishName: /^[^\u4E00-\u9FA5]+$/,
     englishName: /^[^\u4E00-\u9FA5]+$/,
   },
   },
-};
-export default config;
+}
+
+const hostKeys = [
+  'HostWs',
+  'Host80',
+  'Host00',
+  'Host85',
+  'Host04',
+  'Host90',
+  'HostShop',
+  'HostShopImg',
+  'Host87',
+  'Host05',
+  'HostEnter',
+] as const
+
+type HostKey = (typeof hostKeys)[number]
+
+const config: Record<string, unknown> = {
+  ...staticConfig,
+  get ho() {
+    return getDomainParts().ho
+  },
+  get host() {
+    return getDomainParts().ho
+  },
+}
+
+hostKeys.forEach((key) => {
+  Object.defineProperty(config, key, {
+    enumerable: true,
+    configurable: true,
+    get() {
+      return buildHostUrls()[key as HostKey]
+    },
+  })
+})
+
+export default config as typeof staticConfig & Record<HostKey, string> & {
+  ho: string
+  host: string
+}

+ 5 - 5
hooks/useAppUpdate.ts

@@ -5,6 +5,7 @@ import useUserStore from '@/stores/use-user-store'
 import { ucardApi } from '@/api/ucard'
 import { ucardApi } from '@/api/ucard'
 import { userToken } from '@/composables/config'
 import { userToken } from '@/composables/config'
 import config from '@/config'
 import config from '@/config'
+import { whenDomainReady } from '@/utils/dynamicDomain'
 import { trim } from 'lodash'
 import { trim } from 'lodash'
 
 
 // ================== 类型声明 ==================
 // ================== 类型声明 ==================
@@ -212,9 +213,7 @@ function removeStorageSync(key: string): boolean {
 
 
 export function useAppUpdate() {
 export function useAppUpdate() {
   const { t } = useI18n()
   const { t } = useI18n()
-  const { Host80 } = config
-  // 测试环境地址
-  // const Host80 = 'https://secure.44a5c8109e4.com/'
+  // 勿解构 Host80,动态域名下需每次读取 getter
   const userStore = useUserStore()
   const userStore = useUserStore()
   const progress = useProgress()
   const progress = useProgress()
 
 
@@ -244,9 +243,10 @@ export function useAppUpdate() {
     checking.value = true
     checking.value = true
 
 
     try {
     try {
+      await whenDomainReady()
 
 
       const res = await uni.request({
       const res = await uni.request({
-        url: `${Host80}/wgt/list.json?_t=${Date.now()}`,
+        url: `${config.Host80}/wgt/list.json?_t=${Date.now()}`,
         method: 'GET',
         method: 'GET',
         timeout: 5000,
         timeout: 5000,
       })
       })
@@ -288,7 +288,7 @@ export function useAppUpdate() {
       // }
       // }
 
 
       // 显示更新提示
       // 显示更新提示
-      const url = `${Host80}/wgt/CwgApp_${latestVersion}.wgt`
+      const url = `${config.Host80}/wgt/CwgApp_${latestVersion}.wgt`
       console.log('url',url)
       console.log('url',url)
       showForceUpdate({ version: latestVersion, forceUpdate: true, wgtUrl: url })
       showForceUpdate({ version: latestVersion, forceUpdate: true, wgtUrl: url })
     } catch (error) {
     } catch (error) {

+ 4 - 0
main.js

@@ -8,6 +8,10 @@ import vEllipsis from './directives/v-ellipsis'
 import { createI18n } from "vue-i18n";
 import { createI18n } from "vue-i18n";
 import { routeInterceptor } from '@/utils/routeInterceptor.js'
 import { routeInterceptor } from '@/utils/routeInterceptor.js'
 import { lang } from '@/composables/config'
 import { lang } from '@/composables/config'
+// #ifdef APP-PLUS
+import { initDynamicDomain } from '@/utils/dynamicDomain'
+initDynamicDomain()
+// #endif
 // import './static/js/jsvm_all.js'
 // import './static/js/jsvm_all.js'
 import { watch } from "vue";
 import { watch } from "vue";
 import vT from './directives/v-t'
 import vT from './directives/v-t'

+ 1 - 2
pages/common/chat.vue

@@ -15,10 +15,9 @@ import { onLoad } from '@dcloudio/uni-app'
 import useGlobalStore from '@/stores/use-global-store'
 import useGlobalStore from '@/stores/use-global-store'
 const globalStore = useGlobalStore()
 const globalStore = useGlobalStore()
 import Config from '@/config/index'
 import Config from '@/config/index'
-const { Host80 } = Config
 import getWebBase from '@/utils/webBase'
 import getWebBase from '@/utils/webBase'
 const webBase = getWebBase()
 const webBase = getWebBase()
-const fileUrl = `${Host80}/iframe/livechat.html`
+const fileUrl = computed(() => `${Config.Host80}/iframe/livechat.html`)
 const statusBarHeight = computed(() => globalStore.statusBarHeight)
 const statusBarHeight = computed(() => globalStore.statusBarHeight)
 
 
 const webviewStyles = computed(() => ({
 const webviewStyles = computed(() => ({

+ 1 - 2
pages/common/webview.vue

@@ -13,7 +13,6 @@
 import { ref, computed } from 'vue'
 import { ref, computed } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import { onLoad } from '@dcloudio/uni-app'
 import Config from '@/config/index'
 import Config from '@/config/index'
-const { Host80 } = Config
 import getWebBase from '@/utils/webBase'
 import getWebBase from '@/utils/webBase'
 const webBase = getWebBase()
 const webBase = getWebBase()
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
@@ -53,7 +52,7 @@ onLoad((options) => {
 
 
   // ✅ 核心修复:PDF 只编码一次,不再二次编码
   // ✅ 核心修复:PDF 只编码一次,不再二次编码
   if (fileType.value === 'PDF') {
   if (fileType.value === 'PDF') {
-    fileUrl.value = `${Host80}${webBase}/iframe/pdf.html?pdf=${realUrl}&title=${title.value}`
+    fileUrl.value = `${Config.Host80}${webBase}/iframe/pdf.html?pdf=${realUrl}&title=${title.value}`
   } else {
   } else {
     fileUrl.value = realUrl
     fileUrl.value = realUrl
   }
   }

+ 17 - 9
pages/launch/index.vue

@@ -1,15 +1,23 @@
+<template>
+  <view class="launch-page" />
+</template>
+
 <script setup>
 <script setup>
 import { onLoad } from '@dcloudio/uni-app'
 import { onLoad } from '@dcloudio/uni-app'
 import { userToken } from '@/composables/config'
 import { userToken } from '@/composables/config'
+
 onLoad(() => {
 onLoad(() => {
-    if (!userToken.value) {
-        uni.reLaunch({
-            url: '/pages/login/index'
-        })
-    } else {
-        uni.reLaunch({
-            url: '/pages/customer/index'
-        })
-    }
+  if (!userToken.value) {
+    uni.reLaunch({ url: '/pages/login/index' })
+  } else {
+    uni.reLaunch({ url: '/pages/customer/index' })
+  }
 })
 })
 </script>
 </script>
+
+<style scoped>
+.launch-page {
+  min-height: 100vh;
+  background-color: #fff;
+}
+</style>

+ 321 - 0
utils/dynamicDomain.js

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

+ 1 - 2
utils/pdf.js

@@ -1,5 +1,4 @@
 import Config from '@/config/index'
 import Config from '@/config/index'
-const { Host80 } = Config
 import getWebBase from '@/utils/webBase'
 import getWebBase from '@/utils/webBase'
 const webBase = getWebBase()
 const webBase = getWebBase()
 
 
@@ -10,7 +9,7 @@ export function openLocalPdf(fileName, title, type = 'pdf') {
 
 
   // 拼接地址
   // 拼接地址
   if (type === 'pdf') {
   if (type === 'pdf') {
-    targetUrl = `${Host80}/${fileName}`
+    targetUrl = `${Config.Host80}/${fileName}`
   } else if (type === 'pdf1') {
   } else if (type === 'pdf1') {
     targetUrl = fileName
     targetUrl = fileName
   }
   }

+ 8 - 7
utils/request.js

@@ -91,7 +91,7 @@ const responseInterceptor = (response, options = {}) => {
           code: 600,
           code: 600,
         });
         });
       }
       }
-      
+
       // 4. 提示并跳转登录页(防抖/防重复跳转处理)
       // 4. 提示并跳转登录页(防抖/防重复跳转处理)
       if (!isRedirectingToLogin) {
       if (!isRedirectingToLogin) {
         isRedirectingToLogin = true;
         isRedirectingToLogin = true;
@@ -99,14 +99,14 @@ const responseInterceptor = (response, options = {}) => {
           title: "登录已过期,请重新登录",
           title: "登录已过期,请重新登录",
           icon: "none",
           icon: "none",
         });
         });
-        
+
         uni.$emit('logout');
         uni.$emit('logout');
-        
+
         setTimeout(() => {
         setTimeout(() => {
           uni.reLaunch({
           uni.reLaunch({
             url: LOGIN_PAGE_PATH,
             url: LOGIN_PAGE_PATH,
             success: () => {
             success: () => {
-              uni.setStorageSync('logoutToSystem',1)
+              uni.setStorageSync('logoutToSystem', 1)
               ls.set('mode', 'customer');
               ls.set('mode', 'customer');
               // globalStore.setMode('customer');
               // globalStore.setMode('customer');
               // uni.clearStorageSync()
               // uni.clearStorageSync()
@@ -118,7 +118,7 @@ const responseInterceptor = (response, options = {}) => {
           });
           });
         }, 1500);
         }, 1500);
       }
       }
-      
+
       return Promise.reject({
       return Promise.reject({
         ...data,
         ...data,
         code: 600,
         code: 600,
@@ -141,7 +141,7 @@ const responseInterceptor = (response, options = {}) => {
     //   title: `网络错误: ${statusCode}`,
     //   title: `网络错误: ${statusCode}`,
     //   icon: "none",
     //   icon: "none",
     // });
     // });
-    console.log('接口错误error:',error)
+    console.log('接口错误error:', error)
     return Promise.reject(response);
     return Promise.reject(response);
   }
   }
 };
 };
@@ -149,7 +149,7 @@ const responseInterceptor = (response, options = {}) => {
 // 错误处理
 // 错误处理
 const errorHandler = (error) => {
 const errorHandler = (error) => {
   uni.hideLoading();
   uni.hideLoading();
-  console.log('请求失败抛出error:',error)
+  console.log('请求失败抛出error:', error)
   uni.showToast({
   uni.showToast({
     title: "网络异常,请稍后重试",
     title: "网络异常,请稍后重试",
     icon: "none",
     icon: "none",
@@ -161,6 +161,7 @@ const errorHandler = (error) => {
 export const request = async (options) => {
 export const request = async (options) => {
   await whenDomainReady();
   await whenDomainReady();
   const host = getHost(options.type || 'Host80');
   const host = getHost(options.type || 'Host80');
+
   // 合并配置
   // 合并配置
   const config = {
   const config = {
     ...options,
     ...options,