var test_api = 'https://api-test.global-interaction.com/'; var local_api = 'http://127.0.0.1:13000/'; var default_api = 'https://api.global-interaction.com/'; (function () { var API_ENV_KEY = 'hyapp_h5_env'; var ACCESS_TOKEN_KEY = 'hyapp_access_token'; var APP_CODE_KEY = 'hyapp_app_code'; var RENEWED_ACCESS_TOKEN_HEADER = 'X-Hyapp-Access-Token'; var LANGUAGE_QUERY_KEYS = [ 'language', 'lang', 'locale', 'app_lang', 'appLanguage', ]; var memoryEnv = ''; var memoryAccessToken = ''; var memoryAppCode = ''; var activeAppConfig = null; function currentQueryParams() { var params = new URLSearchParams(window.location.search); var hash = window.location.hash || ''; if (hash.indexOf('?') >= 0) { new URLSearchParams(hash.split('?').slice(1).join('?')).forEach( function (value, key) { if (!params.has(key)) params.set(key, value); } ); } return params; } function readQuery(name) { return currentQueryParams().get(name); } function readLanguageQuery() { var params = currentQueryParams(); for (var i = 0; i < LANGUAGE_QUERY_KEYS.length; i += 1) { var value = params.get(LANGUAGE_QUERY_KEYS[i]); if (value) return value; } return ''; } function normalizeToken(value) { var token = String(value || '').trim(); if (!token) return ''; if (/^Bearer\s+/i.test(token)) token = token.replace(/^Bearer\s+/i, ''); var match = token.match( /[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/ ); return match ? match[0] : token; } function decodeBase64URLText(value) { try { var normalized = String(value || '') .replace(/-/g, '+') .replace(/_/g, '/'); while (normalized.length % 4) normalized += '='; return window.atob(normalized); } catch (_) { return ''; } } function decodeBase64URLJSON(value) { try { var text = decodeBase64URLText(value); return text ? JSON.parse(text) : null; } catch (_) { return null; } } function numericUserID(value) { var text = String(value || '').trim(); return /^\d{6,}$/.test(text) ? text : ''; } function firstLongNumericID(value) { var match = String(value || '').match(/\d{12,}/); return match ? match[0] : ''; } function tokenPayloadUserID(payload) { if (!payload || typeof payload !== 'object') return ''; var keys = ['userId', 'user_id', 'uid', 'id', 'sub']; for (var i = 0; i < keys.length; i += 1) { var userID = numericUserID(payload[keys[i]]); if (userID) return userID; } return ''; } function tokenUserID() { var queryUserID = numericUserID(readQuery('userId')) || numericUserID(readQuery('user_id')) || numericUserID(readQuery('uid')); if (queryUserID) return queryUserID; var token = getAccessToken(); if (!token) return ''; var parts = String(token).split('.'); for (var i = 0; i < parts.length; i += 1) { var payloadUserID = tokenPayloadUserID( decodeBase64URLJSON(parts[i]) ); if (payloadUserID) return payloadUserID; var textUserID = firstLongNumericID(decodeBase64URLText(parts[i])); if (textUserID) return textUserID; } return ''; } function isExpiredToken(token) { var parts = String(token || '').split('.'); if (parts.length < 2) return false; var payload = decodeBase64URLJSON(parts[1]); if (!payload || !payload.exp) return false; // JWT exp 是秒级时间戳;本地 H5 不能只凭 query 有 token 就进入 token 模式,过期 token 会被 gateway 当成未登录。 // 这里预留 5 秒偏移,避免页面发请求时刚好跨过过期点,导致 H5 隐藏账号输入但服务端收到空 display_user_id。 return Number(payload.exp) * 1000 <= Date.now() + 5000; } function normalizeBaseURL(value) { return String(value || '').replace(/\/+$/, '') + '/'; } function isLocalDevOrigin() { var host = window.location.hostname; return host === '127.0.0.1' || host === 'localhost' || host === '::1'; } function shouldPersist() { return !isLocalDevOrigin(); } function clearLocalDevCache() { if (shouldPersist()) return; window.localStorage.removeItem(API_ENV_KEY); window.localStorage.removeItem(ACCESS_TOKEN_KEY); window.localStorage.removeItem(APP_CODE_KEY); } function devProxyBase(path) { return normalizeBaseURL(window.location.origin + path); } function localDevProxyBase(env) { if (!isLocalDevOrigin()) return ''; if (env === 'test') return devProxyBase('/__api_test__/'); if (env === 'local') return devProxyBase('/__api_local__/'); // 本地 H5 调试默认仍访问生产数据,但浏览器只能请求同源代理;由 scripts/h5_dev_server.py 代发到生产 API。 return devProxyBase('/__api_prod__/'); } function resolveEnv() { var env = readQuery('env'); if (env === 'test' || env === 'local') { memoryEnv = env; if (shouldPersist()) window.localStorage.setItem(API_ENV_KEY, env); return env; } if (memoryEnv) return memoryEnv; if (!shouldPersist()) return ''; return window.localStorage.getItem(API_ENV_KEY) || ''; } function resolveBaseURL() { var env = resolveEnv(); var configuredAPI = activeAppAPIBaseURL(env); if (configuredAPI) return configuredAPI; var devProxy = localDevProxyBase(env); if (devProxy) return devProxy; if (env === 'test') return normalizeBaseURL(test_api); if (env === 'local') return normalizeBaseURL(local_api); return normalizeBaseURL(default_api); } function activeAppAPIBaseURL(env) { var api = activeAppConfig && activeAppConfig.api; if (!api) return ''; if (env === 'local' && api.local) return normalizeBaseURL(api.local); if (env === 'test' && api.test) return normalizeBaseURL(api.test); return api.prod || api.default ? normalizeBaseURL(api.prod || api.default) : ''; } function buildURL(path, query) { var url = new URL(path.replace(/^\/+/, ''), resolveBaseURL()); if (query) { Object.keys(query).forEach(function (key) { var value = query[key]; if (value === undefined || value === null || value === '') return; url.searchParams.set(key, value); }); } return url.toString(); } function isSameOriginURL(url) { try { return ( new URL(url, window.location.href).origin === window.location.origin ); } catch (_) { return false; } } function getAccessToken() { if (memoryAccessToken && !isExpiredToken(memoryAccessToken)) return memoryAccessToken; if (memoryAccessToken && isExpiredToken(memoryAccessToken)) { memoryAccessToken = ''; if (shouldPersist()) window.localStorage.removeItem(ACCESS_TOKEN_KEY); } var queryToken = normalizeToken(readQuery('token')) || normalizeToken(readQuery('access_token')) || normalizeToken(readQuery('accessToken')); if (queryToken) { if (isExpiredToken(queryToken)) { if (shouldPersist()) window.localStorage.removeItem(ACCESS_TOKEN_KEY); return ''; } memoryAccessToken = queryToken; if (shouldPersist()) window.localStorage.setItem(ACCESS_TOKEN_KEY, queryToken); return queryToken; } if (!shouldPersist()) return ''; var storedToken = normalizeToken( window.localStorage.getItem(ACCESS_TOKEN_KEY) || '' ); if (storedToken && isExpiredToken(storedToken)) { window.localStorage.removeItem(ACCESS_TOKEN_KEY); return ''; } return storedToken; } function themeAppCode(appCode) { appCode = String(appCode || 'lalu') .trim() .toLowerCase(); return appCode === 'yumi' || appCode === 'aslan' ? appCode : 'lalu'; } function requestAppCode() { var queryAppCode = String(readQuery('app_code') || '') .trim() .toLowerCase(); // X-App-Code 只表达本次 URL 显式指定的后端租户,不能复用 getAppCode 的内存或 localStorage 结果。 // 这样没有 app_code 参数的入口会稳定落到 lalu,避免历史主题缓存把后续普通页面继续打到 yumi/aslan。 return queryAppCode || 'lalu'; } function applyAppTheme(appCode) { if (!window.document || !window.document.documentElement) return; window.document.documentElement.setAttribute( 'data-hy-app-code', themeAppCode(appCode) ); } function peekAppCode() { var queryAppCode = readQuery('app') || readQuery('app_code') || readQuery('appCode'); if (queryAppCode) return queryAppCode; if (memoryAppCode) return memoryAppCode; if (!shouldPersist()) return 'lalu'; return window.localStorage.getItem(APP_CODE_KEY) || 'lalu'; } function getAppCode() { var queryAppCode = readQuery('app') || readQuery('app_code') || readQuery('appCode'); if (queryAppCode) { memoryAppCode = queryAppCode; if (shouldPersist()) window.localStorage.setItem(APP_CODE_KEY, queryAppCode); applyAppTheme(queryAppCode); return queryAppCode; } if (memoryAppCode) { applyAppTheme(memoryAppCode); return memoryAppCode; } if (!shouldPersist()) { applyAppTheme('lalu'); return 'lalu'; } var storedAppCode = window.localStorage.getItem(APP_CODE_KEY) || 'lalu'; applyAppTheme(storedAppCode); return storedAppCode; } function setAccessToken(token) { token = normalizeToken(token); if (token && isExpiredToken(token)) token = ''; memoryAccessToken = token || ''; if (token) { if (shouldPersist()) window.localStorage.setItem(ACCESS_TOKEN_KEY, token); return; } if (shouldPersist()) window.localStorage.removeItem(ACCESS_TOKEN_KEY); } function setAppCode(appCode) { memoryAppCode = appCode || ''; applyAppTheme(appCode || 'lalu'); if (appCode) { if (shouldPersist()) window.localStorage.setItem(APP_CODE_KEY, appCode); return; } if (shouldPersist()) window.localStorage.removeItem(APP_CODE_KEY); } applyAppTheme(peekAppCode()); function setAppConfig(config) { activeAppConfig = config || null; if (activeAppConfig && activeAppConfig.app) { setAppCode(activeAppConfig.app); } return activeAppConfig; } function getAppConfig() { return activeAppConfig; } function loadAppConfig(scope) { var appCode = String(getAppCode() || 'lalu') .trim() .toLowerCase(); if (!appCode || appCode === 'lalu') { activeAppConfig = null; return Promise.resolve(null); } var scopePath = String(scope || '').replace(/^\/+|\/+$/g, ''); var configPath = (scopePath ? '/' + scopePath : '.') + '/config/' + appCode + '.json'; var configURL = new URL( configPath, window.location.origin + window.location.pathname ); return window .fetch(configURL.toString(), { cache: shouldPersist() ? 'default' : 'no-store', }) .then(function (response) { if (!response.ok) return null; return response.json(); }) .then(function (config) { return setAppConfig(config); }) .catch(function () { activeAppConfig = null; return null; }); } function errorMessageForResponse(response, payload) { if (response.status === 409) return 'Duplicate request.'; return payload.message || response.statusText || 'request_failed'; } function notifyHTTPError(response, message) { if (response.status !== 409 || !message) return; if (window.HyAppToast && window.HyAppToast.show) { window.HyAppToast.show(message); return; } window.dispatchEvent( new CustomEvent('hyapp:toast', { detail: { message: message }, }) ); } function parseResponse(response) { return response.text().then(function (text) { var payload = text ? JSON.parse(text) : {}; if (!response.ok) { var message = errorMessageForResponse(response, payload); notifyHTTPError(response, message); var httpError = new Error(message); httpError.status = response.status; httpError.code = payload.code; httpError.request_id = payload.request_id; httpError.payload = payload; throw httpError; } var code = payload && payload.code; var errorCode = payload && payload.errorCode; var isErrorCode = (typeof code === 'number' && code !== 0) || (typeof code === 'string' && code !== '' && code !== 'OK') || (typeof errorCode === 'number' && errorCode !== 0) || (typeof errorCode === 'string' && errorCode !== '' && errorCode !== '0' && errorCode !== 'OK'); if (isErrorCode) { var apiError = new Error( payload.message || payload.errorMsg || payload.error || 'api_request_failed' ); apiError.code = payload.code || payload.errorCode; apiError.request_id = payload.request_id; apiError.payload = payload; throw apiError; } return payload && Object.prototype.hasOwnProperty.call(payload, 'data') ? payload.data : payload && Object.prototype.hasOwnProperty.call(payload, 'body') ? payload.body : payload; }); } function persistRenewedAccessToken(response) { var renewedToken = response && response.headers ? response.headers.get(RENEWED_ACCESS_TOKEN_HEADER) : ''; if (renewedToken) setAccessToken(renewedToken); } function request(path, options) { var opts = options || {}; var headers = Object.assign( {}, (activeAppConfig && activeAppConfig.headers) || {}, opts.headers || {} ); var token = getAccessToken(); var url = buildURL(path, opts.query); if (token && !opts.skipAuth) headers.Authorization = 'Bearer ' + token; headers['X-App-Code'] = requestAppCode(); if (!shouldPersist() && isSameOriginURL(url)) headers['Cache-Control'] = 'no-cache'; var body = opts.body; if (body && typeof body === 'object' && !(body instanceof FormData)) { headers['Content-Type'] = headers['Content-Type'] || 'application/json'; body = JSON.stringify(body); } return window .fetch(url, { method: opts.method || 'GET', headers: headers, body: body, cache: shouldPersist() ? 'default' : 'no-store', credentials: opts.credentials || 'omit', }) .then(function (response) { persistRenewedAccessToken(response); return parseResponse(response); }); } var vipAPI = { me: function () { return request('/api/v1/vip/me', { method: 'GET' }); }, packages: function () { return request('/api/v1/vip/packages', { method: 'GET' }); }, purchase: function (payload) { return request('/api/v1/vip/purchase', { method: 'POST', body: payload || {}, }); }, }; var userAPI = { me: function () { return request('/api/v1/users/me', { method: 'GET' }); }, overview: function () { return request('/api/v1/users/me/overview', { method: 'GET' }); }, profile: function (userID, options) { var id = String(userID || '').trim(); var opts = options || {}; if (!id) return Promise.reject(new Error('user_id is required')); return request( '/api/v1/users/by-id/' + encodeURIComponent(id) + '/profile', { method: 'GET', skipAuth: !!opts.skipAuth, } ); }, appearance: function (userID, options) { var id = String(userID || '').trim(); var opts = options || {}; return request( id ? '/api/v1/users/by-id/' + encodeURIComponent(id) + '/appearance' : '/api/v1/users/me/appearance', { method: 'GET', skipAuth: !!opts.skipAuth } ); }, hostIdentity: function () { return request('/api/v1/users/me/host-identity', { method: 'GET', }); }, roleSummary: function () { return request('/api/v1/users/me/role-summary', { method: 'GET', }); }, updateContact: function (payload) { return request('/api/v1/users/me/contact-info', { method: 'POST', body: payload || {}, }); }, completeOnboarding: function (payload) { return request('/api/v1/users/me/onboarding/complete', { method: 'POST', body: payload || {}, }); }, resolveDisplayUserID: function (displayUserID) { return request( '/api/v1/users/by-display-user-id/' + encodeURIComponent(displayUserID || ''), { method: 'GET', } ); }, getInviteReferrer: function () { return request('/api/v1/users/me/invite-referrer', { method: 'GET', }); }, searchInviteReferrer: function (inviteCode) { return request('/api/v1/users/me/invite-referrer/search', { method: 'GET', query: { invite_code: inviteCode || '' }, }); }, bindInviteReferrer: function (inviteCode) { return request('/api/v1/users/me/invite-referrer/bind', { method: 'POST', body: { invite_code: String(inviteCode || '') }, }); }, }; var walletAPI = { balances: function (assetTypes) { var value = Array.isArray(assetTypes) ? assetTypes.join(',') : assetTypes; return request('/api/v1/wallet/me/balances', { method: 'GET', query: { asset_type: value }, }); }, transactions: function (params) { return request('/api/v1/wallet/transactions', { method: 'GET', query: params || {}, }); }, transferCoinFromSeller: function (payload) { return request('/api/v1/wallet/coin-seller/transfer', { method: 'POST', body: payload || {}, }); }, getCoinSellerSubSellerPermission: function () { return request( '/api/v1/wallet/coin-seller/sub-sellers/permission', { method: 'GET', } ); }, inviteCoinSellerSubSeller: function (payload) { return request('/api/v1/wallet/coin-seller/sub-sellers', { method: 'POST', body: payload || {}, }); }, listCoinSellerSubSellers: function (params) { return request('/api/v1/wallet/coin-seller/sub-sellers', { method: 'GET', query: params || {}, }); }, transferCoinSellerToSubSeller: function (payload) { return request('/api/v1/wallet/coin-seller/sub-sellers/transfer', { method: 'POST', body: payload || {}, }); }, }; // H5 充值 API 只做路径和参数名封装;token、app_code、env、错误 envelope 仍由 request 统一处理。 // 金额、金币数、普通/币商钱包、MiFaPay 参数和 USDT 链上校验都以后端订单快照为准。 function isAslanWebPay() { return activeAppConfig && activeAppConfig.adapter === 'aslan-web-pay'; } function isYumiWebPay() { return activeAppConfig && activeAppConfig.adapter === 'yumi-web-pay'; } function isConfiguredWebPay() { return isAslanWebPay() || isYumiWebPay(); } function appEndpoint(name, fallback) { return ( (activeAppConfig && activeAppConfig.endpoints && activeAppConfig.endpoints[name]) || fallback ); } function normalizeCommodityType(value) { var type = String(value || '') .trim() .toUpperCase(); return type === 'FREIGHT_GOLD' || type === 'FREIGHT_GOLD_SUPER' ? type : 'GOLD'; } function configuredCommodityType(extra) { return normalizeCommodityType( (extra && (extra.commodity_type || extra.commodityType || extra.type)) || (activeAppConfig && activeAppConfig.type) || 'GOLD' ); } function aslanUserQuery(displayUserID, extra) { var value = String(displayUserID || '').trim(); var query = { applicationId: activeAppConfig.application_id, type: configuredCommodityType(extra), sysOrigin: activeAppConfig.sys_origin || 'ATYOU', }; if (value) { query.account = value; } if (/^\d+$/.test(value)) { query.userId = value; } return query; } function configuredWebPayUserQuery(displayUserID, extra) { var query = Object.assign(aslanUserQuery(displayUserID, extra), { sysOrigin: activeAppConfig.sys_origin || 'LIKEI', }); if (isYumiWebPay() && !query.userId && !query.account) { // Yumi 的 user-profile 当前只从 query 里的 userId 或 account+sysOrigin 查用户,不会从 Authorization token 反解用户。 // H5 token 入口会隐藏账号输入并传空 displayUserID,因此这里仅在没有显式身份参数时把 token 里的 uid 补成 userId。 query.userId = tokenUserID(); } return query; } function aslanPaymentMethods(options) { var providers = (activeAppConfig && activeAppConfig.providers) || {}; var mifapay = providers.mifapay || {}; var countries = mifapay.countries || []; var countryCode = String( options.country_code || options.countryCode || '' ).toUpperCase(); var matchedCountries = countries.filter(function (country) { return countryMatchesCode(country, countryCode); }); var globalCountries = countries.filter(function (country) { return countryMatchesCode(country, 'GLOBAL'); }); var matchedMethods = collectMifaPayMethods( matchedCountries, mifapay, options, false ); var methods = matchedMethods.concat( collectMifaPayMethods(globalCountries, mifapay, options, false) ); if (!matchedMethods.length && countryCode) { // 用户国家在 app JSON 里没有具体支付方式时,商品仍按用户国家查询,支付方式降级展示 JSON 中全部方式。 // 兜底方式只影响前端展示和用户选择,下单仍带当前用户国家的 payCountryId 供后端校验金额和币种。 methods = collectMifaPayMethods(countries, mifapay, options, true); } return methods; } function collectMifaPayMethods(countries, mifapay, options, fallbackAll) { var methods = []; (countries || []).forEach(function (country) { var countryCode = primaryCountryCode(country); (country.methods || []).forEach(function (method, index) { methods.push( Object.assign({}, method, { method_id: [ mifapay.provider_code || 'mifapay', countryCode, method.pay_way, method.pay_type, index, ].join(':'), provider_code: mifapay.provider_code || 'mifapay', provider_name: mifapay.provider_name || 'MiFaPay', country_code: countryCode, country_name: country.country_name, currency_code: fallbackAll || countryMatchesCode(country, 'GLOBAL') ? options.currency_code || country.currency_code : country.currency_code, usd_to_currency_rate: options.usd_to_currency_rate || options.usdToCurrencyRate || 0, pay_country_id: options.pay_country_id || options.payCountryId || '', status: mifapay.status || 'active', fallback_all_countries: !!fallbackAll, }) ); }); }); return methods; } function primaryCountryCode(country) { country = country || {}; var codes = countryCodeAliases(country); return codes[0] || ''; } function countryMatchesCode(country, code) { code = String(code || '') .trim() .toUpperCase(); if (!code) return false; return countryCodeAliases(country).indexOf(code) >= 0; } function countryCodeAliases(country) { country = country || {}; var rawCodes = [] .concat(country.country_code || []) .concat(country.countryCode || []) .concat(country.country_codes || []) .concat(country.countryCodes || []); var codes = []; rawCodes.forEach(function (value) { if (Array.isArray(value)) { value.forEach(function (item) { pushCountryCodeAlias(codes, item); }); return; } pushCountryCodeAlias(codes, value); }); return codes; } function pushCountryCodeAlias(codes, value) { value = String(value || '') .trim() .toUpperCase(); if (value && codes.indexOf(value) < 0) { codes.push(value); } } function firstValue(object, keys) { object = object || {}; for (var i = 0; i < keys.length; i += 1) { var value = object[keys[i]]; if (value !== undefined && value !== null && value !== '') return value; } return ''; } function normalizeCountryCode(value) { return String(value || '') .trim() .toUpperCase(); } function countryCodeFromCountry(country) { country = country || {}; return normalizeCountryCode( firstValue(country, [ 'alphaTwo', 'alpha_two', 'countryCode', 'country_code', 'code', ]) ); } function countryNameFromCountry(country) { return firstValue(country, [ 'aliasName', 'enName', 'countryName', 'country_name', 'name', ]); } function normalizeAmountUsd(value) { var amount = Number(value || 0); return Number.isFinite(amount) && amount > 0 ? amount : 0; } function decimalToMinor(value) { return Math.round(normalizeAmountUsd(value) * 100); } function booleanValue(value) { if (value === true || value === 1) return true; var text = String(value || '') .trim() .toLowerCase(); return text === 'true' || text === '1' || text === 'yes'; } function mapYumiContext(context) { context = context || {}; var profile = context.userProfile || context.user_profile || {}; var countryList = context.countryList || context.country_list || []; var selectedCountry = countryList[0] || {}; var country = selectedCountry.country || {}; var countryCode = normalizeCountryCode(profile.countryCode || profile.country_code) || countryCodeFromCountry(country); var freightUser = booleanValue( profile.freightUser || profile.freight_user || context.freightUser || context.freight_user ); var commodityType = normalizeCommodityType( profile.commodityType || profile.commodity_type || context.commodityType || context.commodity_type || (freightUser ? 'FREIGHT_GOLD' : 'GOLD') ); freightUser = freightUser || commodityType === 'FREIGHT_GOLD'; return { auth_mode: 'display_user_id', account: { id: profile.id || '', user_id: profile.id || '', userId: profile.id || '', display_user_id: profile.account || profile.id || '', username: profile.userNickname || profile.user_nickname || '', avatar: profile.userAvatar || profile.user_avatar || '', country_code: countryCode, countryCode: countryCode, country_name: profile.countryName || profile.country_name || countryNameFromCountry(country), countryName: profile.countryName || profile.country_name || countryNameFromCountry(country), audience_type: freightUser ? 'coin_seller' : 'ordinary_user', freight_user: freightUser, freightUser: freightUser, commodity_type: commodityType, commodityType: commodityType, }, country_list: countryList.map(function (item) { var itemCountry = item.country || {}; return { id: item.id || '', pay_country_id: item.payCountryId || item.pay_country_id || item.id || '', payCountryId: item.payCountryId || item.pay_country_id || item.id || '', country_id: item.countryId || item.country_id || '', currency_code: item.currency || item.currency_code || '', currencyCode: item.currency || item.currency_code || '', country_code: countryCodeFromCountry(itemCountry), countryCode: countryCodeFromCountry(itemCountry), country_name: countryNameFromCountry(itemCountry), countryName: countryNameFromCountry(itemCountry), country: itemCountry, }; }), commodity_type: commodityType, commodityType: commodityType, freight_user: freightUser, freightUser: freightUser, region_id: context.regionId || context.region_id || '', regionId: context.regionId || context.region_id || '', raw: context, }; } function mapYumiOptions(options) { options = options || {}; var products = (options.commodity || options.products || []).map( function (item) { var content = Number(item.content || 0); var awardContent = Number( item.awardContent || item.award_content || 0 ); var amountUsd = normalizeAmountUsd( item.amountUsd || item.amount_usd ); var localAmount = normalizeAmountUsd( item.amount || item.amountLocal || item.amount_local ); var currencyCode = item.currency || item.currency_code || ''; return { product_id: item.id, productId: item.id, pay_country_id: item.payCountryId || item.pay_country_id || options.payCountryId || options.pay_country_id || '', coin_amount: (Number.isFinite(content) ? content : 0) + (Number.isFinite(awardContent) ? awardContent : 0), base_coin_amount: Number.isFinite(content) ? content : 0, award_coin_amount: Number.isFinite(awardContent) ? awardContent : 0, commodity_type: item.type || item.commodityType || item.commodity_type || options.type || '', commodityType: item.type || item.commodityType || item.commodity_type || options.type || '', amount_usdt: amountUsd, amount_usdt_micro: Math.round(amountUsd * 1000000), amount_minor: decimalToMinor(amountUsd), // Yumi commodity returns both USD amount and local-currency amount; keep both so H5 display does not treat USD as EGP. local_amount: localAmount, localAmount: localAmount, currency_code: currencyCode, currencyCode: currencyCode, raw: item, }; } ); var firstProduct = products[0] || {}; var countryCode = normalizeCountryCode( options.country_code || options.countryCode || '' ); var currencyCode = options.currency_code || options.currencyCode || firstProduct.currency_code || ''; return { products: products, payment_methods: yumiPaymentMethods( Object.assign({}, options, { pay_country_id: options.payCountryId || options.pay_country_id || firstProduct.pay_country_id || '', country_code: countryCode, currency_code: currencyCode, usd_to_currency_rate: options.usd_to_currency_rate || options.usdToCurrencyRate || 0, }) ), pay_country_id: options.payCountryId || options.pay_country_id || firstProduct.pay_country_id || '', country_code: countryCode, currency_code: currencyCode, usd_to_currency_rate: options.usd_to_currency_rate || options.usdToCurrencyRate || 0, usdt_trc20_enabled: false, raw: options, }; } function yumiPaymentMethods(options) { var channels = (options.raw && options.raw.channels) || options.channels || []; var supported = {}; channels.forEach(function (item) { var channel = item.channel || {}; var details = item.details || {}; [ channel.channelCode, channel.channel_code, details.channelCode, details.channel_code, ].forEach(function (code) { if (code) supported[String(code)] = item; }); }); return aslanPaymentMethods(options).filter(function (method) { var channelCode = method.channel_code || [method.pay_way, method.pay_type].filter(Boolean).join('_'); if (!Object.keys(supported).length) return true; return !!supported[channelCode]; }); } function mapYumiOrder(order) { order = order || {}; var factoryCode = String( order.factoryCode || order.factory_code || '' ).toUpperCase(); return { order_id: order.orderId || order.order_id || '', trade_no: order.tradeNo || order.trade_no || '', provider_code: factoryCode === 'MIFA_PAY' ? 'mifapay' : factoryCode, factory_code: factoryCode, pay_url: order.requestUrl || order.request_url || order.pay_url || '', currency_code: order.currency || order.currency_code || '', country_code: order.countryCode || order.country_code || '', provider_amount_minor: decimalToMinor(order.amount), status: 'pending', raw: order, }; } function mapYumiOrderStatus(order) { order = order || {}; var status = String(order.status || '').toUpperCase(); var normalized = order.success === true || status === 'SUCCESS' ? 'credited' : order.finished === true || status === 'FAIL' ? 'failed' : 'pending'; var factoryCode = String( order.factoryCode || order.factory_code || '' ).toUpperCase(); return { order_id: order.orderId || order.order_id || '', trade_no: order.tradeNo || order.trade_no || '', provider_code: factoryCode === 'MIFA_PAY' ? 'mifapay' : factoryCode, factory_code: factoryCode, currency_code: order.currency || order.currency_code || '', provider_amount_minor: decimalToMinor(order.amount), status: normalized, failure_reason: order.reason || '', raw: order, }; } var rechargeAPI = { context: function (displayUserID) { if (isAslanWebPay()) { return request( appEndpoint('context', '/order/web/pay/h5/context'), { method: 'GET', query: aslanUserQuery(displayUserID), } ); } if (isYumiWebPay()) { return request( appEndpoint('context', '/order/web/pay/user-profile'), { method: 'GET', query: configuredWebPayUserQuery(displayUserID), } ).then(mapYumiContext); } return request('/api/v1/recharge/h5/context', { method: 'GET', query: { display_user_id: displayUserID || '' }, }); }, options: function (displayUserID, extra) { if (isConfiguredWebPay()) { var query = Object.assign( aslanUserQuery(displayUserID, extra), { payCountryId: extra && (extra.pay_country_id || extra.payCountryId), countryCode: extra && (extra.country_code || extra.countryCode), regionId: extra && (extra.region_id || extra.regionId), userId: extra && (extra.user_id || extra.userId), } ); return request( appEndpoint('options', '/order/web/pay/h5/options'), { method: 'GET', query: query, } ).then(function (options) { options = options || {}; if (isYumiWebPay()) { return mapYumiOptions( Object.assign({}, options, { pay_country_id: query.payCountryId || options.pay_country_id || options.payCountryId || '', country_code: query.countryCode || options.country_code, }) ); } options.payment_methods = aslanPaymentMethods(options); options.usdt_trc20_enabled = false; return options; }); } return request('/api/v1/recharge/h5/options', { method: 'GET', query: { display_user_id: displayUserID || '' }, }); }, createOrder: function (payload) { if (isAslanWebPay()) { return request( appEndpoint('orders', '/order/web/pay/h5/orders'), { method: 'POST', body: { applicationId: activeAppConfig.application_id, goodsId: payload && payload.product_id, payCountryId: payload && payload.pay_country_id, userId: payload && payload.user_id, providerCode: payload && payload.provider_code, payWay: payload && payload.pay_way, payType: payload && payload.pay_type, channelCode: payload && payload.channel_code, returnUrl: payload && payload.return_url, commandId: payload && payload.command_id, type: configuredCommodityType(payload), language: readLanguageQuery() || 'en', }, } ); } if (isYumiWebPay()) { return request( appEndpoint('orders', '/order/web/pay/recharge'), { method: 'POST', body: { applicationId: activeAppConfig.application_id, goodsId: payload && payload.product_id, payCountryId: payload && payload.pay_country_id, userId: payload && payload.user_id, channelCode: payload && payload.channel_code, newVersion: true, appVersion: 'h5', }, } ).then(mapYumiOrder); } return request('/api/v1/recharge/h5/orders', { method: 'POST', query: { language: readLanguageQuery() }, body: payload || {}, }); }, submitTx: function (orderID, payload) { return request( '/api/v1/recharge/h5/orders/' + encodeURIComponent(orderID || '') + '/tx', { method: 'POST', body: payload || {}, } ); }, getOrder: function (orderID, displayUserID) { if (isAslanWebPay()) { return request( appEndpoint( 'order', '/order/web/pay/h5/orders/{orderId}' ).replace('{orderId}', encodeURIComponent(orderID || '')), { method: 'GET' } ); } if (isYumiWebPay()) { return request( appEndpoint('order', '/order/web/pay/order-status'), { method: 'GET', query: { orderId: orderID || '', userId: displayUserID || '', }, } ).then(mapYumiOrderStatus); } return request( '/api/v1/recharge/h5/orders/' + encodeURIComponent(orderID || ''), { method: 'GET', query: { display_user_id: displayUserID || '' }, } ); }, }; var salaryWalletAPI = { overview: function (identity) { return request('/api/v1/salary-wallet/overview', { method: 'GET', query: { identity: identity }, }); }, history: function (identity, limit) { return request('/api/v1/salary-wallet/history', { method: 'GET', query: { identity: identity, page_size: limit || 30, }, }); }, searchCoinSeller: function (displayUserID, identity) { return request('/api/v1/salary-wallet/coin-sellers/search', { method: 'GET', query: { display_user_id: displayUserID, identity: identity, }, }); }, exchange: function (payload) { return request('/api/v1/salary-wallet/exchange', { method: 'POST', body: payload || {}, }); }, transferToCoinSeller: function (payload) { return request('/api/v1/salary-wallet/transfer-to-coin-seller', { method: 'POST', body: payload || {}, }); }, getWithdrawAddress: function () { return request('/api/v1/salary-wallet/withdraw-address', { method: 'GET', }); }, saveWithdrawAddress: function (payload) { return request('/api/v1/salary-wallet/withdraw-address', { method: 'POST', body: payload || {}, }); }, withdraw: function (payload) { return request('/api/v1/salary-wallet/withdraw', { method: 'POST', body: payload || {}, }); }, }; var hostAPI = { searchAgencies: function (shortId, pageSize) { return request('/api/v1/host/agencies/search', { method: 'GET', query: { short_id: shortId, page_size: pageSize || 20 }, }); }, applyToAgency: function (payload) { return request('/api/v1/host/agency-applications', { method: 'POST', body: payload || {}, }); }, }; var agencyCenterAPI = { overview: function () { return request('/api/v1/agency-center/overview', { method: 'GET', }); }, platformPolicy: function () { return request('/api/v1/agency-center/platform-policy', { method: 'GET', }); }, hosts: function () { return request('/api/v1/agency-center/hosts', { method: 'GET', }); }, applications: function (status) { return request('/api/v1/agency-center/applications', { method: 'GET', query: { status: status || 'pending' }, }); }, reviewApplication: function (applicationId, payload) { return request( '/api/v1/agency-center/applications/' + encodeURIComponent(applicationId || '') + '/review', { method: 'POST', body: payload || {}, } ); }, removeHost: function (hostUserId, payload) { return request( '/api/v1/agency-center/hosts/' + encodeURIComponent(hostUserId || '') + '/remove', { method: 'POST', body: payload || {}, } ); }, inviteHost: function (payload) { return request('/api/v1/agency-center/invitations/host', { method: 'POST', body: payload || {}, }); }, }; var hostCenterAPI = { agency: function () { return request('/api/v1/host-center/agency', { method: 'GET', }); }, platformPolicy: function () { return request('/api/v1/host-center/platform-policy', { method: 'GET', }); }, }; var bdLeaderCenterAPI = { overview: function () { return request('/api/v1/bd-leader-center/overview', { method: 'GET', }); }, bds: function (pageSize) { return request('/api/v1/bd-leader-center/bds', { method: 'GET', query: { page_size: pageSize || 20 }, }); }, agencies: function (pageSize) { return request('/api/v1/bd-leader-center/agencies', { method: 'GET', query: { page_size: pageSize || 20 }, }); }, inviteBD: function (payload) { return request('/api/v1/bd-leader/invitations/bd', { method: 'POST', body: payload || {}, }); }, inviteAgency: function (payload) { return request('/api/v1/bd-leader/invitations/agency', { method: 'POST', body: payload || {}, }); }, }; var bdCenterAPI = { overview: function () { return request('/api/v1/bd-center/overview', { method: 'GET', }); }, agencies: function (pageSize) { return request('/api/v1/bd-center/agencies', { method: 'GET', query: { page_size: pageSize || 20 }, }); }, inviteAgency: function (payload) { return request('/api/v1/bd/invitations/agency', { method: 'POST', body: payload || {}, }); }, }; var roleInvitationAPI = { list: function (status, pageSize) { return request('/api/v1/role-invitations', { method: 'GET', query: { status: status || 'pending', page_size: pageSize || 50, }, }); }, process: function (invitationID, payload) { return request( '/api/v1/role-invitations/' + encodeURIComponent(invitationID || '') + '/process', { method: 'POST', body: payload || {}, } ); }, }; var managerCenterAPI = { overview: function () { return request('/api/v1/manager-center/overview', { method: 'GET', }); }, searchUsers: function (keyword, pageSize, scope, action) { return request('/api/v1/manager-center/users/search', { method: 'GET', query: { keyword: keyword, page_size: pageSize || 20, scope: scope, action: action, }, }); }, listCountries: function () { return request('/api/v1/countries', { method: 'GET', }); }, listResources: function (resourceType, pageSize) { return request('/api/v1/manager-center/resource-grants/resources', { method: 'GET', query: { resource_type: resourceType, page_size: pageSize || 20, }, }); }, grantResource: function (payload) { return request('/api/v1/manager-center/resource-grants', { method: 'POST', body: payload || {}, }); }, grantVip: function (payload) { return request('/api/v1/manager-center/vip-grants', { method: 'POST', body: payload || {}, }); }, listBlocks: function (status, pageSize) { return request('/api/v1/manager-center/blocks', { method: 'GET', query: { status: status || 'active', page_size: pageSize || 50, }, }); }, blockUser: function (payload) { return request('/api/v1/manager-center/blocks', { method: 'POST', body: payload || {}, }); }, unblockUser: function (blockId, payload) { return request( '/api/v1/manager-center/blocks/' + encodeURIComponent(blockId || '') + '/unblock', { method: 'POST', body: payload || {}, } ); }, updateLevel: function (payload) { return request('/api/v1/manager-center/levels', { method: 'POST', body: payload || {}, }); }, createBDLeader: function (payload) { return request('/api/v1/manager-center/bd-leaders', { method: 'POST', body: payload || {}, }); }, transferUserCountry: function (payload) { return request('/api/v1/manager-center/users/country', { method: 'POST', body: payload || {}, }); }, }; var superadminCenterAPI = bdLeaderCenterAPI; var levelAPI = { overview: function () { return request('/api/v1/levels/me/overview', { method: 'GET' }); }, track: function (track) { return request( '/api/v1/levels/tracks/' + encodeURIComponent(track || ''), { method: 'GET', } ); }, rewards: function (query) { return request('/api/v1/levels/rewards', { method: 'GET', query: query || {}, }); }, }; // 成就页只读当前用户的 App 成就和徽章展示资料;认证、app_code、env、错误 envelope 仍由 request 统一处理。 // collection_type 由页面按 tab 传入,避免 H5 自己拼接不同业务集合导致后端筛选语义漂移。 var achievementAPI = { list: function (query, options) { var opts = options || {}; return request('/api/v1/achievements', { method: 'GET', query: query || {}, skipAuth: !!opts.skipAuth, }); }, badges: function () { return request('/api/v1/badges/me', { method: 'GET' }); }, setDisplay: function (payload) { return request('/api/v1/badges/display', { method: 'POST', body: payload || {}, }); }, }; // 任务页 API 只封装 H5 需要的两个 App 合约;登录态、app_code、env 和错误 envelope 仍由 request 统一处理。 // 领取奖励的 command_id 必须来自点击动作本身,页面脚本负责生成并在同一次重试中复用,避免重复点击造成多次发奖。 var taskAPI = { tabs: function () { return request('/api/v1/tasks/tabs', { method: 'GET' }); }, claim: function (payload) { return request('/api/v1/tasks/claim', { method: 'POST', body: payload || {}, }); }, }; var weeklyStarAPI = { current: function () { return request('/api/v1/activities/weekly-star/current', { method: 'GET', }); }, leaderboard: function (params) { return request('/api/v1/activities/weekly-star/leaderboard', { method: 'GET', query: params || {}, }); }, history: function (limit) { return request('/api/v1/activities/weekly-star/history', { method: 'GET', query: { limit: limit || 10 }, }); }, }; var cpWeeklyRankAPI = { status: function (params) { return request('/api/v1/activities/cp-weekly-rank', { method: 'GET', query: params || {}, }); }, }; var cpSpaceAPI = { relationships: function (params) { return request('/api/v1/cp/relationships', { method: 'GET', query: params || {}, }); }, intimacyLeaderboard: function (params) { return request('/api/v1/cp/intimacy-leaderboard', { method: 'GET', query: params || {}, }); }, breakRelationship: function (relationshipID, payload) { return request( '/api/v1/cp/relationships/' + encodeURIComponent(relationshipID || '') + '/break', { method: 'POST', body: payload || {}, } ); }, }; // 转盘 H5 只通过 gateway 活动接口读配置、抽奖、查记录和跑马灯提示;页面不再内置 mock 奖品,避免后台资源组未配置时前端仍展示假奖池。 // 抽奖 command_id 由页面点击时生成并随请求提交,服务端用它做幂等,网络重试不会重复扣费或重复发奖。 var activityWheelAPI = { config: function (wheelID) { return request( '/api/v1/activity/wheel/' + encodeURIComponent(wheelID || 'classic') + '/config', { method: 'GET' } ); }, draw: function (wheelID, payload) { return request( '/api/v1/activity/wheel/' + encodeURIComponent(wheelID || 'classic') + '/draw', { method: 'POST', body: payload || {}, } ); }, history: function (wheelID, params) { return request( '/api/v1/activity/wheel/' + encodeURIComponent(wheelID || 'classic') + '/history', { method: 'POST', query: params || {}, body: {}, } ); }, hints: function (wheelID) { return request( '/api/v1/activity/wheel/' + encodeURIComponent(wheelID || 'classic') + '/hints', { method: 'POST', body: {}, } ); }, }; // 邀请活动 H5 只关心当前用户本月状态和领取动作;有效人数、累计充值和最高档差额都以后端聚合事实为准。 // 领取时页面必须传入本次点击生成的 command_id,服务端按用户、周期、档位和 command_id 做幂等发奖。 var inviteActivityRewardAPI = { status: function () { return request('/api/v1/activities/invite-reward', { method: 'GET', }); }, leaderboard: function (params) { return request('/api/v1/activities/invite-reward/leaderboard', { method: 'GET', query: params || {}, }); }, claim: function (payload) { return request('/api/v1/activities/invite-reward/claim', { method: 'POST', body: payload || {}, }); }, }; var diceGameAPI = { config: function (gameID) { return request('/api/v1/games/dice/config', { method: 'GET', query: { game_id: gameID || 'dice' }, }); }, match: function (payload) { return request('/api/v1/games/dice/match', { method: 'POST', body: payload || {}, }); }, getMatch: function (matchID) { return request( '/api/v1/games/dice/matches/' + encodeURIComponent(matchID || ''), { method: 'GET' } ); }, roll: function (matchID) { return request( '/api/v1/games/dice/matches/' + encodeURIComponent(matchID || '') + '/roll', { method: 'POST', body: {} } ); }, cancel: function (matchID) { return request( '/api/v1/games/dice/matches/' + encodeURIComponent(matchID || '') + '/cancel', { method: 'POST', body: {} } ); }, }; var rpsGameAPI = { config: function (gameID) { return request('/api/v1/games/rps/config', { method: 'GET', query: { game_id: gameID || 'rock' }, }); }, match: function (payload) { return request('/api/v1/games/rps/match', { method: 'POST', body: payload || {}, }); }, create: function (payload) { return request('/api/v1/games/rps/matches', { method: 'POST', body: payload || {}, }); }, join: function (matchID, payload) { return request( '/api/v1/games/rps/matches/' + encodeURIComponent(matchID || '') + '/join', { method: 'POST', body: payload || {} } ); }, getMatch: function (matchID) { return request( '/api/v1/games/rps/matches/' + encodeURIComponent(matchID || ''), { method: 'GET' } ); }, roll: function (matchID, payload) { return request( '/api/v1/games/rps/matches/' + encodeURIComponent(matchID || '') + '/roll', { method: 'POST', body: payload || {} } ); }, cancel: function (matchID) { return request( '/api/v1/games/rps/matches/' + encodeURIComponent(matchID || '') + '/cancel', { method: 'POST', body: {} } ); }, }; function analyticsEventID(prefix) { var random = ''; if (window.crypto && window.crypto.getRandomValues) { var bytes = new Uint32Array(2); window.crypto.getRandomValues(bytes); random = bytes[0].toString(36) + bytes[1].toString(36); } else { random = Math.random().toString(36).slice(2); } return ( String(prefix || 'self_game') + '_' + Date.now().toString(36) + '_' + random ); } function reportSelfGameEvent(payload) { var body = Object.assign({}, payload || {}); body.event_id = body.event_id || analyticsEventID(body.event_name); body.occurred_at_ms = body.occurred_at_ms || Date.now(); return request('/api/v1/games/self/report-events', { method: 'POST', body: body, }); } var gameAPI = { dice: diceGameAPI, rps: rpsGameAPI, reportEvent: reportSelfGameEvent, }; window.HyAppAPI = { baseURL: resolveBaseURL, buildURL: buildURL, getAccessToken: getAccessToken, getAppCode: getAppCode, setAccessToken: setAccessToken, setAppCode: setAppCode, setAppConfig: setAppConfig, getAppConfig: getAppConfig, loadAppConfig: loadAppConfig, request: request, get: function (path, query) { return request(path, { method: 'GET', query: query }); }, post: function (path, body, query) { return request(path, { method: 'POST', body: body, query: query }); }, put: function (path, body, query) { return request(path, { method: 'PUT', body: body, query: query }); }, patch: function (path, body, query) { return request(path, { method: 'PATCH', body: body, query: query }); }, delete: function (path, query) { return request(path, { method: 'DELETE', query: query }); }, vip: vipAPI, user: userAPI, wallet: walletAPI, recharge: rechargeAPI, salaryWallet: salaryWalletAPI, host: hostAPI, hostCenter: hostCenterAPI, agencyCenter: agencyCenterAPI, bdCenter: bdCenterAPI, bdLeaderCenter: bdLeaderCenterAPI, roleInvitations: roleInvitationAPI, managerCenter: managerCenterAPI, superadminCenter: superadminCenterAPI, activityWheel: activityWheelAPI, level: levelAPI, achievement: achievementAPI, task: taskAPI, weeklyStar: weeklyStarAPI, cpWeeklyRank: cpWeeklyRankAPI, cpSpace: cpSpaceAPI, inviteActivityReward: inviteActivityRewardAPI, game: gameAPI, }; clearLocalDevCache(); })();