const ApiClient = { // 后端租户标识,所有接口请求都会带到 header 里。 // 后面如果租户号更换,只需要改这里的默认值,或在 config.js 里配置 CURRENT_CONFIG.TENANT_ID。 TENANT_ID: '531923', // TENANT_ID: '000000', API: { authCode: '/api/web/auth/code', authLogin: '/api/web/auth/login', authLogout: '/api/web/auth/logout', authRegister: '/api/web/auth/register', authSmsCode: '/api/web/auth/sms/code', authWechatOpen: '/api/web/auth/binding/wechat_open', authVerifyConfig: '/api/web/auth/verify-config', authSlideCaptcha: '/api/web/auth/slide-captcha', authSlideCaptchaVerify: '/api/web/auth/slide-captcha/verify', authHeartbeat: '/api/web/auth/heartbeat', authForgotPasswordSmsCode: '/api/web/auth/forgot-password/sms-code', authForgotPasswordReset: '/api/web/auth/forgot-password/reset', authSocialLogin: '/api/web/auth/social-login', authSocialCallback: '/api/web/auth/social-callback', authSocialBind: '/api/web/auth/social-bind', authSocialBindPhone: '/api/web/auth/social-bind-phone', authSocialBindPhoneSmsCode: '/api/web/auth/social-bind-phone/sms-code', appDownloadInfo: '/api/web/app/getDownloadInfo', agreement: (type) => `/api/web/agreement/${encodeURIComponent(type || '')}`, noticeList: '/api/web/notice/list', noticeDetail: (id) => `/api/web/notice/${encodeURIComponent(id || '')}`, friendLinks: '/api/web/link/friend', otherRecommendLinks: '/api/web/link/other-recommend', latestLotteryResult: '/api/web/lottery/result/latest', lotteryResultList: '/api/web/lottery/result/list', webConfigList: '/api/web/config/list', webConfig: '/api/web/config', configList: '/api/web/config/list', configSite: '/api/web/config/site', configCopyright: '/api/web/config/copyright', bannerList: '/api/web/banner/list', adList: '/api/web/ad/list', freeArticleRecommend: '/api/web/free-article/recommend', freeArticleList: '/api/web/free-article/list', freeArticleLatest: '/api/web/free-article/latest', referralShareInfo: '/api/web/referral/share-info', referralSummary: '/api/web/referral/summary', referralInviteList: '/api/web/referral/invite-list', referralRebateList: '/api/web/referral/rebate-list', referralBindPreview: '/api/web/referral/bind-preview', referralBind: '/api/web/referral/bind', accountInfo: '/api/web/account/info', accountProfile: '/api/web/account/profile', accountProfileAvatar: '/api/web/account/profile/avatar', expertConfig: '/api/web/expert/config', expertApply: '/api/web/expert/apply', expertUploadIdCardFront: '/api/web/expert/upload-id-card-front', expertUploadIdCardBack: '/api/web/expert/upload-id-card-back', homeNav: '/api/web/menu/home-nav', menuChildren: (id) => `/api/web/menu/${encodeURIComponent(id)}/children`, payArticleExpertProfile: '/api/web/pay-article/expert-profile', payArticleExpertList: '/api/web/pay-article/expert-list', payArticleExpertRank: '/api/web/pay-article/expert-rank', payArticlePublishOptions: (menuId) => `/api/web/pay-article/publish/options?menuId=${encodeURIComponent(menuId || '')}`, payArticlePublish: '/api/web/pay-article/publish', payArticleLatest: '/api/web/pay-article/latest', payArticleRelated: (id) => `/api/web/pay-article/${encodeURIComponent(id || '')}/related`, payArticleCollect: (id) => `/api/web/pay-article/article-collect/${encodeURIComponent(id)}`, expertCollect: (expertId, accountType = 1) => `/api/web/pay-article/expert-collect?${new URLSearchParams({ expertId: String(expertId || ''), accountType: String(accountType || 1), }).toString()}`, walletOverview: '/api/web/wallet/overview', walletFlowList: '/api/web/wallet/flow/list', walletFlowDelete: (id) => `/api/web/wallet/flow/${encodeURIComponent(id)}`, walletRechargeConfigList: '/api/web/wallet/recharge/config/list', walletRechargeOrder: '/api/web/wallet/recharge/order', walletRechargeOrderStatus: (outTradeNo) => `/api/web/wallet/recharge/order/status/${encodeURIComponent(outTradeNo || '')}`, walletRechargeNotify: '/api/web/wallet/recharge/notify', walletTransferToAccount: '/api/web/wallet/transfer-to-account', walletWithdraw: '/api/web/wallet/withdraw', walletWithdrawList: '/api/web/wallet/withdraw/list', walletPrepurchasePayArticle: '/api/web/wallet/prepurchase/pay-article', walletReceiveAccountList: '/api/web/wallet/receive-account/list', walletReceiveAccount: '/api/web/wallet/receive-account', walletReceiveAccountUpdate: (id) => `/api/web/wallet/receive-account/${encodeURIComponent(id)}`, walletReceiveAccountDefault: (id) => `/api/web/wallet/receive-account/${encodeURIComponent(id)}/default`, walletReceiveAccountDelete: (id) => `/api/web/wallet/receive-account/${encodeURIComponent(id)}`, walletReceiveAccountUploadQrcode: '/api/web/wallet/receive-account/upload-qrcode', mineSummary: '/api/web/mine/summary', mineFreeCollectList: '/api/web/mine/free-collect/list', minePayCollectList: '/api/web/mine/pay-collect/list', mineExpertCollectList: '/api/web/mine/expert-collect/list', mineSignInList: '/api/web/mine/sign-in/list', mineSignInStatus: '/api/web/mine/sign-in/status', mineSignIn: '/api/web/mine/sign-in', walletPurchasePayArticle: '/api/web/wallet/purchase/pay-article', freeArticlePublish: '/api/web/free-article/publish', mineFreeArticleList: '/api/web/mine/free-article/list', mineFreeArticleDetail: (id) => `/api/web/mine/free-article/${encodeURIComponent(id || '')}`, minePayArticleList: '/api/web/mine/pay-article/list', minePayArticleDetail: (id) => `/api/web/mine/pay-article/${encodeURIComponent(id)}`, minePurchasedPayArticleList: '/api/web/mine/purchased-pay-article/list', minePurchasedPayArticleDelete: (sourceType, recordId) => `/api/web/mine/purchased-pay-article/${encodeURIComponent(sourceType)}/${encodeURIComponent(recordId)}`, freeArticleDetail: (id) => `/api/web/free-article/${encodeURIComponent(id)}`, freeArticleRelated: (id) => `/api/web/free-article/${encodeURIComponent(id)}/related`, freeArticleComments: (id) => `/api/web/free-article/${encodeURIComponent(id)}/comments`, freeArticleComment: (id) => `/api/web/free-article/${encodeURIComponent(id)}/comment`, freeArticleLike: (id) => `/api/web/free-article/${encodeURIComponent(id)}/like`, freeArticleCollect: (id) => `/api/web/free-article/${encodeURIComponent(id)}/collect`, fc3dFreePredictMenus: '/api/web/fc3d/free-predict/menus', fc3dFreePredictList: '/api/web/fc3d/free-predict/list', fc3dSchemeList: '/api/web/fc3d/scheme/list', fc3dSchemeDetail: (id) => `/api/web/fc3d/scheme/${encodeURIComponent(id)}`, walletPurchaseFc3dScheme: '/api/web/wallet/purchase/fc3d-scheme', lotteryTrendTools: '/api/web/lottery/result/trend/tools', lotteryTrend: '/api/web/lottery/result/trend', }, get baseUrl() { return (window.CURRENT_CONFIG && window.CURRENT_CONFIG.API_BASE_URL) || ''; }, get clientId() { return ( (window.CURRENT_CONFIG && window.CURRENT_CONFIG.CLIENT_ID) || '209ef407810e3856f40870a9f0e769d7' ); }, get CLIENT_ID() { return this.clientId; }, get tenantId() { return ( (window.CURRENT_CONFIG && window.CURRENT_CONFIG.TENANT_ID) || this.TENANT_ID ); }, request(options) { const normalized = this.normalizeLegacyRequest(options || {}); if (normalized.compose) return normalized.compose(); const method = (normalized.method || 'GET').toUpperCase(); const url = this.buildUrl(normalized.url, method === 'GET' ? normalized.data : null); const dedupeKey = this.requestDedupeKey(method, url, normalized); const cached = this.getDedupeCache(dedupeKey); if (cached) return cached; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), normalized.timeout || 10000); const baseHeaders = normalized.publicRequest ? this.publicHeaders() : this.defaultHeaders(); const config = { method, headers: { ...baseHeaders, ...(normalized.headers || {}), }, signal: controller.signal, }; if (normalized.data && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { config.body = normalized.data instanceof FormData || typeof normalized.data === 'string' ? normalized.data : JSON.stringify(normalized.data); if (normalized.data instanceof FormData) delete config.headers['Content-Type']; } const promise = fetch(this.baseUrl + url, config) .then(async (response) => { clearTimeout(timeoutId); if (!response.ok) { if (response.status === 401) { localStorage.removeItem('token'); NavigationUtil.open('login.html'); } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = this.normalizeResponse(await this.parseResponse(response), normalized); if (this.isAuthExpiredResponse(result)) this.handleAuthExpired(); return result; }) .catch((error) => { clearTimeout(timeoutId); if (error.name === 'AbortError') throw new Error('Request timeout'); throw error; }); return this.setDedupeCache(dedupeKey, promise); }, requestDedupeKey(method, url, normalized = {}) { if (method !== 'GET' || normalized.dedupe === false) return ''; const token = this.getToken() || ''; return [method, this.baseUrl + url, token, this.clientId, this.tenantId].join(' '); }, getDedupeStore() { if (!this._dedupeStore) this._dedupeStore = new Map(); return this._dedupeStore; }, getDedupeCache(key) { if (!key) return null; const store = this.getDedupeStore(); const item = store.get(key); if (!item) return null; if (item.pending) return item.promise; if (Date.now() - item.time > 500) { store.delete(key); return null; } return item.promise; }, setDedupeCache(key, promise) { if (!key) return promise; const store = this.getDedupeStore(); store.set(key, { promise, pending: true, time: Date.now() }); promise .then(() => { const item = store.get(key); if (item?.promise === promise) { item.pending = false; item.time = Date.now(); setTimeout(() => { if (store.get(key)?.promise === promise) store.delete(key); }, 600); } }) .catch(() => { if (store.get(key)?.promise === promise) store.delete(key); }); return promise; }, defaultHeaders() { const headers = { 'Content-Type': 'application/json', tenantId: this.tenantId, }; const token = this.getToken(); if (token) { headers.Authorization = `Bearer ${token}`; headers.clientid = this.clientId; } return headers; }, publicHeaders() { return { 'Content-Type': 'application/json', tenantId: this.tenantId, }; }, isSuccess(res) { return !!res && (res.code === 0 || res.code === 200 || res.success === true); }, isAuthExpiredResponse(result) { if (!result || typeof result !== 'object') return false; const code = Number(result.code); const message = String(result.msg || result.message || result.error || '').toLowerCase(); return ( code === 401 || code === 403 || code === 40101 || code === 40102 || message.indexOf('token') >= 0 && (message.indexOf('expired') >= 0 || message.indexOf('invalid') >= 0) ); }, getToken() { const raw = localStorage.getItem('token'); if (!raw) return ''; try { const parsed = JSON.parse(raw); return parsed.access_token || parsed.accessToken || parsed.token || parsed; } catch (e) { return raw; } }, token() { return this.getToken(); }, clearToken() { localStorage.removeItem('token'); localStorage.removeItem('userInfo'); localStorage.removeItem('user'); }, getLocalUserInfo() { const raw = localStorage.getItem('userInfo') || localStorage.getItem('user') || ''; if (!raw) return {}; try { return JSON.parse(raw) || {}; } catch (e) { return {}; } }, isJwtExpired(token) { const parts = String(token || '').split('.'); if (parts.length < 2) return false; try { let payloadText = parts[1].replace(/-/g, '+').replace(/_/g, '/'); while (payloadText.length % 4) payloadText += '='; const payload = JSON.parse(decodeURIComponent(escape(window.atob(payloadText)))); return payload.exp ? payload.exp * 1000 <= Date.now() : false; } catch (e) { return false; } }, isTokenExpired() { const token = this.token(); if (!token) return true; if (this.isJwtExpired(token)) return true; const userInfo = this.getLocalUserInfo(); const expireTime = Number(userInfo.expireTime || userInfo.expire_time || userInfo.expiresAt || userInfo.expires_at || 0); return expireTime ? expireTime <= Date.now() : false; }, handleAuthExpired() { if (typeof window === 'undefined') return; this.clearToken(); const path = window.location.pathname || ''; if (/\/(login|reg|repwd)\.html$/i.test(path)) return; const returnUrl = window.location.pathname + window.location.search; const loginPath = '/login.html?redirect=' + encodeURIComponent(returnUrl); NavigationUtil.open( typeof CommonUtil !== 'undefined' && CommonUtil.siteHref ? CommonUtil.siteHref(loginPath) : ((window.CURRENT_ENV === 'development' ? '/html/login.html?redirect=' : '/login.html?redirect=') + encodeURIComponent(returnUrl))); }, buildUrl(url, params) { if (!params || params instanceof FormData) return url; const clean = this.cleanParams(params); const query = new URLSearchParams(clean).toString(); if (!query) return url; return url + (url.indexOf('?') >= 0 ? '&' : '?') + query; }, cleanParams(params, omittedKeys = []) { const clean = {}; const omitted = new Set(omittedKeys || []); Object.keys(params || {}).forEach((key) => { const value = params[key]; if (omitted.has(key)) return; if (value !== undefined && value !== null && value !== '') clean[key] = value; }); return clean; }, async parseResponse(response) { const contentType = response.headers.get('content-type') || ''; if (contentType.indexOf('application/json') >= 0) return response.json(); return response.text(); }, normalizeResponse(result, context = {}) { if (!result || typeof result !== 'object') return result; const normalized = { ...result }; if (normalized.code === 200) { normalized.rawCode = 200; normalized.code = 0; } if (typeof normalized.success === 'undefined') { normalized.success = normalized.code === 0 || normalized.code === 200; } if (Array.isArray(normalized.rows)) { normalized.data = { records: normalized.rows, rows: normalized.rows, totalRow: normalized.total || 0, total: normalized.total || 0, }; } if (context.url === '/api/web/pay-article/expert-rank') { this.normalizeExpertRankResponse(normalized, context.data || {}); } if (normalized.data && typeof normalized.data === 'object') { if (Object.prototype.hasOwnProperty.call(normalized.data, 'captchaEnabled')) { normalized.data.id = normalized.data.uuid || 'captcha-disabled'; normalized.data.image = normalized.data.img || this.transparentGif(); if (normalized.data.captchaEnabled === false) this.applyCaptchaDisabledState(); } if (normalized.data.access_token && !normalized.data.token) { normalized.data.token = normalized.data.access_token; } } return normalized; }, normalizeExpertRankResponse(result, params = {}) { const data = result.data && typeof result.data === 'object' ? result.data : {}; const source = Array.isArray(data.records) ? data.records : Array.isArray(data.rows) ? data.rows : Array.isArray(result.rows) ? result.rows : []; const rows = source.map((item, index) => { const successRate = item.successRate === undefined || item.successRate === null ? '' : String(item.successRate); const hitRate = item.hitRate || (successRate ? `${successRate}%` : ''); return { ...item, rank: item.rank || index + 1, createBy: item.createBy || item.expertId, expertId: item.expertId || item.createBy, createUserName: item.createUserName || item.nickname || item.expertName, nickname: item.nickname || item.createUserName || item.expertName, issue: item.issue || item.latestIssue, title: item.title || item.latestTitle || item.latestRecommend, hitRate, }; }); const shouldExpandForLegacyRank = params && !params.pageNum && Number(params.pageSize || 0) >= 50; const records = shouldExpandForLegacyRank ? rows.flatMap((item) => { const total = Math.max(Number(item.totalCount) || 1, 1); const hit = Math.max(Number(item.correctCount) || 0, 0); return Array.from({ length: total }, (_, index) => ({ ...item, status: index < hit ? '1' : '0', })); }) : rows; result.data = { ...data, records, rows: records, totalRow: data.totalRow || data.total || result.total || rows.length, total: data.total || data.totalRow || result.total || rows.length, }; }, transparentGif() { return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; }, applyCaptchaDisabledState() { if (typeof window === 'undefined' || !window.document) return; setTimeout(() => { const input = document.getElementById('captchaInput'); const hidden = document.getElementById('captchaId'); const box = document.getElementById('captchaContainer'); if (input) input.value = '0000'; if (hidden) hidden.value = 'captcha-disabled'; if (box) box.style.display = 'none'; }, 0); }, normalizePageParams(data) { const params = { ...(data || {}) }; if (params.page && !params.pageNum) params.pageNum = params.page; if (params.limit && !params.pageSize) params.pageSize = params.limit; if (params.pageNo && !params.pageNum) params.pageNum = params.pageNo; delete params.page; delete params.limit; delete params.pageNo; return params; }, pickData(result, fallback) { if (!result || typeof result !== 'object') return fallback; return result.data === undefined || result.data === null ? fallback : result.data; }, asArray(value) { if (Array.isArray(value)) return value; if (!value || typeof value !== 'object') return []; if (Array.isArray(value.records)) return value.records; if (Array.isArray(value.rows)) return value.rows; if (Array.isArray(value.list)) return value.list; if (Array.isArray(value.data)) return value.data; return []; }, unwrap(result, fallback) { return this.pickData(result, fallback); }, listFrom(source, keys = ['records', 'rows', 'list', 'data']) { if (Array.isArray(source)) return source; if (!source || typeof source !== 'object') return []; for (const key of keys || []) { const nested = this.asArray(source[key]); if (nested.length) return nested; } return []; }, normalizeLegacyRequest(options) { const url = options.url || ''; const method = (options.method || 'GET').toUpperCase(); const data = options.data || {}; if (url === '/api/web/index') { return { compose: () => this.loadHomeIndexCompat() }; } if (url === '/api/web/menu/getFormatList') { return { compose: () => this.loadMenuCompat() }; } if (url === '/api/web/link/getList') { return { compose: () => this.loadLinksCompat() }; } if (url === '/api/web/QRCode') { return { compose: () => Promise.resolve({ code: 0, success: true, data: location.origin + '/html/appdown.html' }) }; } if (url === '/api/web/user/login') { return { compose: () => this.loginCompat(data) }; } if (url.indexOf('/api/user/collect/list') === 0) { return { ...options, url: '/api/web/mine/free-collect/list', method: 'GET', data: this.normalizePageParams({ ...data, page: this.queryValue(url, 'page') || data.page, pageSize: this.queryValue(url, 'pageSize') || data.pageSize, }), }; } if (url === '/api/web/lotteryResult/getList') { return { compose: () => this.requestLegacyLotteryResultCompat(data) }; } if (url === '/api/web/freeArticle/getPageList') { return { compose: () => this.requestWithMenu('/api/web/free-article/list', data) }; } if (url === '/api/web/payArticle/getPageList') { return { compose: () => this.requestWithMenu('/api/web/pay-article/expert-rank', data) }; } if (url === '/api/web/payArticle/getListByCreateUser') { return { compose: () => this.loadLegacyExpertDetailCompat(data), }; } const direct = this.mapSimpleLegacyUrl(url, data); if (direct) return { ...options, ...direct }; if (url.indexOf('/api/web/agreement/getByType') === 0) { const type = this.queryValue(url, 'type') || data.type || '1'; return { ...options, url: `/api/web/agreement/${type}`, method: 'GET', data: null }; } if (url.indexOf('/api/web/notice/getDetail') === 0) { const id = this.queryValue(url, 'id') || data.id; return { ...options, url: `/api/web/notice/${id}`, method: 'GET', data: null }; } if (url.indexOf('/api/web/freeArticle/getDetail') === 0) { const id = this.queryValue(url, 'id') || data.id; return { ...options, url: `/api/web/free-article/${id}`, method: 'GET', data: null }; } return { ...options, method, data }; }, mapSimpleLegacyUrl(url, data) { const pageParams = this.normalizePageParams(data); const map = { '/api/web/user/register': { url: '/api/web/auth/register', method: 'POST', data: this.normalizeAuthPayload(data) }, '/api/web/user/getCaptcha': { url: '/api/web/auth/code', method: 'GET', data: null }, '/api/web/user/logout': { url: '/api/web/auth/logout', method: 'POST', data: null }, '/api/web/user/getCurrentUserDetail': { url: '/api/web/mine/summary', method: 'GET', data: null }, '/api/web/user/getUserBalance': { url: '/api/web/wallet/overview', method: 'GET', data: null }, '/api/web/user/signIn': { url: '/api/web/mine/sign-in', method: 'POST', data: null }, '/api/web/user/signIn/getPageList': { url: '/api/web/mine/sign-in/list', method: 'GET', data: pageParams }, '/api/web/capitalFlow/getPageList': { url: '/api/web/wallet/flow/list', method: 'GET', data: pageParams }, '/api/web/payment/getConfigList': { url: '/api/web/wallet/recharge/config/list', method: 'GET', data: null }, '/api/web/payment/charge/native': { url: '/api/web/wallet/recharge/order', method: 'POST', data }, '/api/web/freeArticle/getPageListByUser': { url: '/api/web/mine/free-article/list', method: 'GET', data: pageParams }, '/api/web/freeArticle/push': { url: '/api/web/free-article/publish', method: 'POST', data }, '/api/web/payArticle/push': { url: '/api/web/pay-article/publish', method: 'POST', data: { parentId: data.parentId, code: data.code, title: data.title, predictedCode: data.predictedCode || data.predictCode || data.content, issue: data.issue || data.expect, }, }, '/api/web/payArticle/subscribe': { url: '/api/web/wallet/purchase/pay-article', method: 'POST', data }, '/api/web/order/getPageListByUser': { url: '/api/web/mine/purchased-pay-article/list', method: 'GET', data: pageParams }, '/api/web/rebate/getAffInfo': { url: '/api/web/referral/share-info', method: 'GET', data: null }, '/api/web/rebate/getPageList': { url: '/api/web/referral/rebate-list', method: 'GET', data: pageParams }, '/api/web/rebate/getWithdrawList': { url: '/api/web/wallet/withdraw/list', method: 'GET', data: pageParams }, '/api/web/rebate/withdraw': { url: '/api/web/wallet/withdraw', method: 'POST', data }, '/api/web/rebate/rebateToBalance': { url: '/api/web/wallet/transfer-to-account', method: 'POST', data }, '/api/web/distribution/getPageList': { url: '/api/web/referral/invite-list', method: 'GET', data: pageParams }, '/api/web/user/applyDistribution': { url: '/api/web/referral/bind', method: 'POST', data }, '/api/web/user/applyExpert': { url: '/api/web/expert/apply', method: 'POST', data }, '/api/web/user/expert/apply': { url: '/api/web/expert/apply', method: 'POST', data }, '/api/web/user/updateInfo': { url: '/api/web/account/profile', method: 'PUT', data }, '/api/web/notice/getPageList': { url: '/api/web/notice/list', method: 'GET', data: pageParams }, '/api/web/fc3d/freePredict/list': { url: '/api/web/fc3d/free-predict/list', method: 'GET', data: pageParams }, '/api/web/fc3d/scheme/list': { url: '/api/web/fc3d/scheme/list', method: 'GET', data: pageParams }, '/api/web/fc3d/scheme/buy': { url: '/api/web/wallet/purchase/fc3d-scheme', method: 'POST', data }, }; if (map[url]) return map[url]; const pathMap = [ [/^\/api\/web\/freeArticle\/like\/(.+)$/, (id) => ({ url: `/api/web/free-article/${id}/like`, method: 'POST', data: null })], [/^\/api\/web\/freeArticle\/collect\/(.+)$/, (id) => ({ url: `/api/web/free-article/${id}/collect`, method: 'POST', data: null })], [/^\/api\/web\/freeArticleComment\/getPageList$/, () => ({ url: `/api/web/free-article/${data.articleId}/comments`, method: 'GET', data: pageParams })], [/^\/api\/web\/freeArticleComment\/push$/, () => ({ url: `/api/web/free-article/${data.articleId}/comment`, method: 'POST', data })], [/^\/api\/web\/payArticle\/collect\/(.+)$/, (id) => ({ url: `/api/web/pay-article/article-collect/${id}`, method: 'POST', data: null })], [/^\/api\/web\/expert\/collect\/(.+)$/, (id) => ({ url: `/api/web/pay-article/expert-collect?${new URLSearchParams({ expertId: id, accountType: data.accountType || 1, }).toString()}`, method: 'POST', data: null, })], ]; for (const [pattern, build] of pathMap) { const match = url.match(pattern); if (match) return build(match[1]); } return null; }, normalizeAuthPayload(data) { const payload = { ...(data || {}) }; if (!payload.grantType) payload.grantType = 'password'; if (!payload.deviceType) payload.deviceType = 'WEB'; if (payload.captcha && !payload.code) payload.code = payload.captcha; if (payload.captchaId && payload.captchaId !== 'captcha-disabled' && !payload.uuid) { payload.uuid = payload.captchaId; } if (payload.captchaId === 'captcha-disabled') { payload.verifyType = payload.verifyType || 'none'; delete payload.uuid; delete payload.captchaId; delete payload.captcha; delete payload.code; } else if (!payload.verifyType) { payload.verifyType = payload.code ? 'captcha' : 'none'; } return payload; }, async loginCompat(data) { const payload = this.normalizeAuthPayload(data); const verifyConfig = await this.get('/api/web/auth/verify-config').catch(() => ({ data: null })); const verifyType = verifyConfig.data && verifyConfig.data.verifyType; if (verifyType === 'slide' || verifyConfig.data?.slideCaptchaEnabled) { const slide = await this.get('/api/web/auth/slide-captcha').catch(() => ({ data: null })); if (slide.data && slide.data.uuid) { payload.verifyType = 'slide'; payload.slideUuid = slide.data.uuid; payload.slideX = slide.data.targetX || slide.data.x || 0; delete payload.code; delete payload.uuid; delete payload.captcha; delete payload.captchaId; } } return this.post('/api/web/auth/login', payload); }, queryValue(url, key) { const query = url.split('?')[1]; if (!query) return ''; return new URLSearchParams(query).get(key) || ''; }, fetchHomeNav() { if (!this._homeNavPromise) { this._homeNavPromise = this.get('/api/web/menu/home-nav').catch((error) => { this._homeNavPromise = null; throw error; }); } return this._homeNavPromise; }, normalizeMenuList(navData, options = {}) { return (navData || []) .filter((item) => item.type === 'lottery') .map((item) => ({ ...item, suoxie: item.suoxie || item.code, url: item.url || item.path, children: options.keepChildren && Array.isArray(item.children) ? item.children : [], })); }, shouldLoadMenuChildren() { if (typeof window === 'undefined') return true; return /\/(?:cai|caiinfo|fufei)\.html(?:$|\?)/.test( window.location.pathname + window.location.search ); }, async loadMenuCompat() { if (!this._menuCompatPromise) { this._menuCompatPromise = (async () => { const nav = await this.fetchHomeNav(); const list = this.normalizeMenuList(nav.data, { keepChildren: true }); if (!this.shouldLoadMenuChildren()) { return { code: 0, success: true, data: list }; } const missingChildren = list.filter((item) => item.id && !item.children.length); await Promise.all( missingChildren.map((item) => this.get(`/api/web/menu/${item.id}/children`) .then((res) => { item.children = res.data || []; }) .catch(() => { item.children = []; }) ) ); return { code: 0, success: true, data: list }; })().catch((error) => { this._menuCompatPromise = null; throw error; }); } return this._menuCompatPromise; }, async loadHomeMenusCompat() { const nav = await this.fetchHomeNav(); const list = this.normalizeMenuList(nav.data); return { code: 0, success: true, data: list }; }, async requestWithMenu(url, data) { const params = this.normalizePageParams(data || {}); if (url === '/api/web/pay-article/expert-rank' && !params.issueCount && params.pageSize) { params.issueCount = params.pageSize; if (params.pageNum) params.pageSize = params.rowPageSize || 10; } params.menuId = params.menuId || params.parentId || params.typeId || (await this.resolveMenuId(params.code || params.suoxie)); delete params.parentId; delete params.typeId; delete params.code; delete params.suoxie; return this.get(url, params); }, async requestLegacyLotteryResultCompat(data = {}) { const params = this.normalizePageParams(data || {}); const code = params.code || params.suoxie || params.lotteryCode || ''; const pageSize = params.pageSize || params.limit || params.count || 100; const normalizeResponse = (res) => { if (!this.isSuccess(res)) return res; const payload = this.pickData(res, {}); const list = this.asArray(payload); return Object.assign({}, res, { code: 0, success: true, data: list, rawData: payload, rows: list, records: list, }); }; if (code && this.API.lotteryTrend) { try { const trendRes = await this.get(this.API.lotteryTrend, { code, pageSize, }); if (this.isSuccess(trendRes) && this.asArray(this.pickData(trendRes, [])).length) { return normalizeResponse(trendRes); } } catch (e) { // Fall back to the older result-list endpoint below. } } const listRes = await this.requestWithMenu('/api/web/lottery/result/list', data); return normalizeResponse(listRes); }, async loadLegacyExpertDetailCompat(data = {}) { const cacheKey = JSON.stringify(this.normalizePageParams(data)); if (!this._legacyExpertDetailPromises) this._legacyExpertDetailPromises = {}; if (this._legacyExpertDetailPromises[cacheKey]) { return this._legacyExpertDetailPromises[cacheKey]; } this._legacyExpertDetailPromises[cacheKey] = this.fetchLegacyExpertDetailCompat(data).catch( (error) => { delete this._legacyExpertDetailPromises[cacheKey]; throw error; } ); return this._legacyExpertDetailPromises[cacheKey]; }, async fetchLegacyExpertDetailCompat(data = {}) { const params = this.normalizePageParams(data); const expertId = params.expertId || params.createBy || ''; const accountType = params.accountType || 1; const menuId = params.menuId || params.parentId || params.typeId || (await this.resolveMenuId(params.code || params.suoxie)); const listParams = { ...params, expertId, accountType, menuId, pageNum: params.pageNum || 1, pageSize: params.pageSize || 50, }; delete listParams.createBy; delete listParams.parentId; delete listParams.typeId; delete listParams.code; delete listParams.suoxie; const [profileRes, listRes] = await Promise.all([ expertId ? this.get('/api/web/pay-article/expert-profile', { expertId, accountType }).catch((error) => ({ code: 500, msg: error.message || '获取专家资料失败', data: {}, })) : Promise.resolve({ code: 0, data: {} }), this.get('/api/web/pay-article/expert-list', listParams), ]); if (listRes.code !== 0) return listRes; const profile = this.pickData(profileRes, {}) || {}; const listData = this.pickData(listRes, {}) || {}; const list = this.asArray(listData).map((item) => ({ ...item, id: item.id || item.articleId || item.payArticleId, issue: item.issue || item.latestIssue || item.expect || item.period, title: item.title || item.latestTitle, predictedCode: item.predictedCode || item.recommendCode || item.latestRecommend || item.recommend || item.content, openCode: item.openCode || item.lotteryResult || item.resultCode || item.openResult, accountType: item.accountType || accountType, })); return { ...listRes, data: { ...listData, ...profile, expertId, createBy: expertId, menuId, parentId: menuId, accountType, avatar: profile.avatar || profile.headImg, nickname: profile.nickname || profile.nickName || profile.expertName, isCollect: profile.isCollect ?? profile.collected ?? profile.expertCollected, payArticleList: list, records: list, rows: list, totalRow: listData.totalRow || listData.total || listRes.total || list.length, total: listData.total || listData.totalRow || listRes.total || list.length, }, }; }, async resolveMenuId(code) { if (!code) return ''; if (!this._menuIdCache) { const nav = await this.fetchHomeNav().catch(() => ({ data: [] })); this._menuIdCache = {}; (nav.data || []).forEach((item) => { if (item.code && item.id) this._menuIdCache[item.code] = item.id; }); } return this._menuIdCache[code] || ''; }, async loadLinksCompat() { const [friend, other] = await Promise.all([ this.get('/api/web/link/friend').catch(() => ({ data: [] })), this.get('/api/web/link/other-recommend').catch(() => ({ data: [] })), ]); const friendList = (friend.data || []).map((item) => ({ ...item, type: 1 })); const otherList = (other.data || []).map((item) => ({ ...item, type: 2 })); return { code: 0, success: true, data: friendList.concat(otherList) }; }, async loadHomeIndexCompat() { const [configList, config, copyright, siteConfig, menus, banners, notices, ads, freeArticles, links, latest] = await Promise.all([ this.get('/api/web/config/list').catch(() => ({ data: [] })), this.get('/api/web/config').catch(() => ({ data: {} })), this.get(this.API.configCopyright).catch(() => ({ data: [] })), this.get('/api/web/config/site').catch(() => ({ data: {} })), this.loadHomeMenusCompat().catch(() => ({ data: [] })), this.get('/api/web/banner/list').catch(() => ({ data: [] })), this.get('/api/web/notice/list', { count: 6 }).catch(() => ({ data: [] })), this.get('/api/web/ad/list', { inTime: true }).catch(() => ({ data: [] })), this.get('/api/web/free-article/recommend', { articleCount: 5 }).catch(() => ({ data: [] })), this.loadLinksCompat().catch(() => ({ data: [] })), this.get('/api/web/lottery/result/latest').catch(() => ({ data: [] })), ]); const latestMap = {}; const lotteryList = this.asArray(this.pickData(latest, [])); lotteryList.forEach((item) => { latestMap[item.code] = { ...item, expect: item.expect, name: item.expect, time: item.time, openCode: item.openCode, code: item.openCode, red: item.redNumbers, blue: item.blueNumbers, }; }); const articleGroups = this.asArray(this.pickData(freeArticles, [])); const articleMap = {}; articleGroups.forEach((group) => { if (Array.isArray(group.articles)) { articleMap[group.code] = group.articles.map((article) => ({ ...article, code: article.code || group.code, lotteryName: article.lotteryName || group.lotteryName, accountAvatar: article.accountAvatar || article.expertAvatar || article.avatar, createUserName: article.createUserName || article.author || article.nickName || article.issuer, })); return; } const code = group.code || group.suoxie; if (!code) return; if (!articleMap[code]) articleMap[code] = []; articleMap[code].push({ ...group, accountAvatar: group.accountAvatar || group.expertAvatar || group.avatar, createUserName: group.createUserName || group.author || group.nickName || group.issuer, }); }); const menuList = this.asArray(this.pickData(menus, [])).map((menu) => ({ ...menu, suoxie: menu.suoxie || menu.code, url: menu.url || menu.path, lotteryResult: latestMap[menu.suoxie || menu.code], freeArticleList: articleMap[menu.suoxie || menu.code] || [], })); return { code: 0, success: true, data: { webConfigs: this.asArray(this.pickData(configList, [])).length ? this.asArray(this.pickData(configList, [])) : this.configObjectToList(config.data || {}), copyrightConfigs: this.asArray(this.pickData(copyright, [])), siteConfig: siteConfig.data || {}, menuList, navList: menuList, lotteryList, freeArticleData: articleGroups, bannerList: this.asArray(this.pickData(banners, [])), noticeList: this.asArray(this.pickData(notices, [])), adList: this.asArray(this.pickData(ads, [])), linkList: this.asArray(this.pickData(links, [])), }, }; }, configObjectToList(config) { return Object.keys(config || {}).map((key, index) => ({ type: key, content: config[key], sort: index, })); }, get(url, params, options = {}) { if (typeof options === 'function') { return this.request({ url, method: 'GET', data: params || null }).then(options); } return this.request({ url, method: 'GET', data: params || null, ...options }); }, post(url, data, options = {}) { if (typeof options === 'function') { return this.request({ url, method: 'POST', data: data || null }).then(options); } return this.request({ url, method: 'POST', data: data || null, ...options }); }, put(url, data, options = {}) { if (typeof options === 'function') { return this.request({ url, method: 'PUT', data: data || null }).then(options); } return this.request({ url, method: 'PUT', data: data || null, ...options }); }, delete(url, data, options = {}) { if (typeof options === 'function') { return this.request({ url, method: 'DELETE', data: data || null }).then(options); } return this.request({ url, method: 'DELETE', data: data || null, ...options }); }, };