import { API_OPERATIONS, apiEndpointPath } from "../../src/shared/api/generated/endpoints.ts"; import { API_BASE_URL } from "../../src/shared/config/env.js"; import { countryFlag, stripCountryFlagPrefix } from "./data/countryMeta.js"; const TOKEN_KEY = "hyapp-admin.access-token"; const APP_CODE_KEY = "hyapp-admin.app-code"; const APP_CODE_HEADER = "X-App-Code"; const DEFAULT_APP_CODE = "lalu"; const ALL_APP_OPTION = { code: "", label: "全部", value: "" }; const DEFAULT_APP_OPTION = { code: DEFAULT_APP_CODE, label: "Lalu", value: DEFAULT_APP_CODE }; const FIXED_APP_OPTIONS = [ { code: "aslan", label: "Aslan", value: "aslan" }, { code: "yumi", label: "Yumi", value: "yumi" } ]; const SOCIAL_BI_ALL_VALUE = "all"; const SOCIAL_BI_OPERATION_TEAM_ID = 2; export function getDefaultAppOptions() { return mergeAppOptions([ALL_APP_OPTION, DEFAULT_APP_OPTION, ...FIXED_APP_OPTIONS]); } export function getCurrentAppCode() { const stored = window.localStorage.getItem(APP_CODE_KEY); return stored === null ? DEFAULT_APP_CODE : normalizeAppCode(stored); } export function setCurrentAppCode(value) { const appCode = normalizeAppCode(value); window.localStorage.setItem(APP_CODE_KEY, appCode); return appCode; } export async function fetchStatisticsOverview({ appCode, countryId, endMs, regionId, seriesEndMs, seriesStartMs, startMs, statTz }) { const query = appScopedQuery(appCode); if (statTz) { query.stat_tz = statTz; } if (startMs) { query.start_ms = String(startMs); } if (endMs) { query.end_ms = String(endMs); } if (seriesStartMs) { query.series_start_ms = String(seriesStartMs); } if (seriesEndMs) { query.series_end_ms = String(seriesEndMs); } if (countryId) { query.country_id = String(countryId); } if (regionId && regionId !== "all") { query.region_id = String(regionId); } return fetchDatabiData(apiEndpointPath(API_OPERATIONS.statisticsOverview), { appCode, query }); } export async function fetchPlatformGrantUsers({ appCode, countryId, endMs, page, pageSize, regionId, startMs, statDay, statTz }) { return fetchDatabiData(apiEndpointPath(API_OPERATIONS.listPlatformGrantUsers), { appCode, query: platformGrantQuery({ appCode, countryId, endMs, page, pageSize, regionId, startMs, statDay, statTz }) }); } export async function fetchPlatformGrantRecords({ appCode, countryId, endMs, page, pageSize, regionId, source, startMs, statDay, statTz, userId }) { return fetchDatabiData(apiEndpointPath(API_OPERATIONS.listPlatformGrantRecords), { appCode, query: platformGrantQuery({ appCode, countryId, endMs, page, pageSize, regionId, source, startMs, statDay, statTz, userId }) }); } export async function fetchSelfGameStatisticsOverview({ appCode, countryId, endMs, gameId, regionId, startMs, statTz }) { const query = appScopedQuery(appCode); if (statTz) { query.stat_tz = statTz; } if (startMs) { query.start_ms = String(startMs); } if (endMs) { query.end_ms = String(endMs); } if (countryId) { query.country_id = String(countryId); } if (regionId && regionId !== "all") { query.region_id = String(regionId); } if (gameId && gameId !== "all") { query.game_id = String(gameId); } return fetchDatabiData(apiEndpointPath(API_OPERATIONS.selfGameStatisticsOverview), { appCode, query }); } function platformGrantQuery({ appCode, countryId, endMs, page, pageSize, regionId, source, startMs, statDay, statTz, userId }) { const query = appScopedQuery(appCode); if (statTz) { query.stat_tz = statTz; } if (startMs) { query.start_ms = String(startMs); } if (endMs) { query.end_ms = String(endMs); } if (statDay) { query.stat_day = statDay; } if (countryId) { query.country_id = String(countryId); } if (regionId && regionId !== "all") { query.region_id = String(regionId); } if (userId) { query.user_id = String(userId); } if (source && source !== "all") { query.source = String(source); } if (page) { query.page = String(page); } if (pageSize) { query.page_size = String(pageSize); } return query; } export async function fetchFilterOptions({ appCode } = {}) { const [apps, regions, countries] = await Promise.all([ fetchDatabiData(apiEndpointPath(API_OPERATIONS.listApps), { appCode }), fetchDatabiData(apiEndpointPath(API_OPERATIONS.listRegions), { appCode, query: { status: "active" } }), fetchDatabiData(apiEndpointPath(API_OPERATIONS.listCountries), { appCode, query: { enabled: true } }) ]); return normalizeFilterOptions({ apps, countries, regions }); } export async function fetchSocialBiFilterOptions({ appCode } = {}) { const [apps, operators] = await Promise.all([ fetchDatabiData(apiEndpointPath(API_OPERATIONS.listApps), { appCode }), fetchDatabiData(apiEndpointPath(API_OPERATIONS.listTeamUsers, { team_id: SOCIAL_BI_OPERATION_TEAM_ID }), { appCode, query: { page_size: 200, status: "active" } }) ]); return { appOptions: normalizeSocialBiAppOptions(apps), operatorOptions: normalizeSocialBiOperatorOptions(operators) }; } // 社交 BI 主数据:按当前登录用户的数据范围返回可见 App、区域目录、运营人员及权限开关。 export async function fetchSocialBiMaster() { return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiMaster)); } export async function fetchSocialBiOverview({ appCodes, endMs, regionId, regionIds, startMs, statTz }) { const query = {}; if (statTz) { query.stat_tz = statTz; } if (startMs) { query.start_ms = String(startMs); } if (endMs) { query.end_ms = String(endMs); } if (appCodes?.length) { query.app_codes = appCodes.join(","); } if (regionId) { query.region_id = String(regionId); } else if (regionIds?.length) { query.region_ids = regionIds.join(","); } return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiOverview), { query }); } export async function fetchSocialBiRequirements({ appCodes, endMs, newUserType, payerType, regionId, regionIds, section, startMs, statTz, userRole }) { const query = {}; if (statTz) { query.stat_tz = statTz; } if (startMs) { query.start_ms = String(startMs); } if (endMs) { query.end_ms = String(endMs); } if (appCodes?.length) { query.app_codes = appCodes.join(","); } if (regionId) { query.region_id = String(regionId); } else if (regionIds?.length) { query.region_ids = regionIds.join(","); } if (section) { query.section = section; } if (userRole) { query.user_role = userRole; } if (payerType) { query.payer_type = payerType; } if (newUserType) { query.new_user_type = newUserType; } return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiRequirements), { query }); } export async function fetchSocialBiFunnel({ appCodes, endMs, regionId, startMs, statTz }) { const query = {}; if (statTz) { query.stat_tz = statTz; } if (startMs) { query.start_ms = String(startMs); } if (endMs) { query.end_ms = String(endMs); } if (appCodes?.length) { query.app_codes = appCodes.join(","); } if (regionId) { query.region_id = String(regionId); } return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiFunnel), { query }); } export async function fetchSocialBiKpi({ appCodes, endMs, operatorUserId, periodMonth, startMs, statTz }) { const query = {}; if (statTz) { query.stat_tz = statTz; } if (startMs) { query.start_ms = String(startMs); } if (endMs) { query.end_ms = String(endMs); } if (periodMonth) { query.period_month = periodMonth; } if (appCodes?.length) { query.app_codes = appCodes.join(","); } if (operatorUserId) { query.operator_user_id = String(operatorUserId); } return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiKpi), { query }); } async function fetchDatabiData(path, { appCode, body, method, query } = {}) { const headers = requestHeaders(appCode); const init = { credentials: "include", headers, method: method || "GET" }; if (body !== undefined) { headers["Content-Type"] = "application/json"; init.body = JSON.stringify(body); } const response = await fetch(buildURL(path, query), init); const payload = await readJSON(response); if (isAuthExpired(response, payload)) { redirectToLogin(window); throw new Error(payload.message || "访问凭证已失效"); } if (!response.ok || payload.code !== 0) { throw new Error(payload.message || response.statusText || "请求失败"); } return payload.data; } function buildURL(path, query) { const url = new URL(`${API_BASE_URL}${path}`, window.location.origin); if (query) { Object.entries(query).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== "") { url.searchParams.set(key, String(value)); } }); } return url.toString(); } function requestHeaders(appCode) { const headers = {}; const token = window.localStorage.getItem(TOKEN_KEY); if (token) { headers.Authorization = `Bearer ${token}`; } const normalized = normalizeAppCode(appCode); if (normalized) { headers[APP_CODE_HEADER] = normalized; } return headers; } async function readJSON(response) { const text = await response.text(); if (!text) { return {}; } try { return JSON.parse(text); } catch { return { code: response.ok ? 0 : response.status, message: text }; } } export function isAuthExpired(response, payload) { return response.status === 401 || Number(payload?.code) === 40100; } export function redirectToLogin(targetWindow = window) { targetWindow.localStorage.removeItem(TOKEN_KEY); if (targetWindow.location.pathname !== "/login") { targetWindow.location.replace("/login"); } } function normalizeAppCode(value) { return String(value || "").trim().toLowerCase(); } function appScopedQuery(appCode) { const normalized = normalizeAppCode(appCode); // “全部”使用空 appCode,让请求层省略 X-App-Code 和 app_code,避免把 all 当成真实 App 传给后端。 return normalized ? { app_code: normalized } : {}; } function normalizeFilterOptions({ apps, countries, regions }) { const regionItems = normalizeList(regions); const regionOptions = [ { countryCodes: [], id: "all", label: "全部区域", value: "all" }, ...regionItems .filter((region) => String(region.regionCode || "").toUpperCase() !== "GLOBAL") .map((region) => { const id = String(region.regionId ?? region.id ?? region.regionCode ?? ""); return { countryCodes: normalizeCodes(region.countries), id, label: [region.name, region.regionCode].filter(Boolean).join(" · ") || `区域 ${id}`, value: id }; }) .filter((region) => region.id) ]; const countryRegionIds = buildCountryRegionIndex(regionOptions); const countryOptions = [ { countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 }, ...normalizeList(countries) .map((country) => normalizeCountryOption(country, countryRegionIds)) .filter(Boolean) ]; const apiAppOptions = normalizeList(apps) .map((app) => { const code = normalizeAppCode(app.appCode ?? app.app_code ?? app.code); return { code, label: app.appName || app.app_name || app.name || code, value: code }; }) .filter((app) => app.code); const appOptions = mergeAppOptions([ALL_APP_OPTION, ...apiAppOptions, DEFAULT_APP_OPTION, ...FIXED_APP_OPTIONS]); return { appOptions, countryOptions, regionOptions }; } function normalizeSocialBiAppOptions(apps) { const options = normalizeList(apps) .map((app) => { const code = normalizeAppCode(app.appCode ?? app.app_code ?? app.code); if (!code) { return null; } return { label: app.appName || app.app_name || app.name || code, value: code }; }) .filter(Boolean); const fixedOptions = FIXED_APP_OPTIONS.map((app) => ({ label: app.label, value: app.value })); return [{ label: "全部 App", value: SOCIAL_BI_ALL_VALUE }, ...dedupeOptions([...options, ...fixedOptions])]; } function normalizeSocialBiOperatorOptions(operators) { const options = normalizeList(operators) .map((user) => { const id = user.id ?? user.userId ?? user.user_id ?? user.account; const label = user.name || user.nickname || user.account || String(id || ""); if (!id || !label) { return null; } return { label, searchText: [label, user.account, user.teamName, user.team].filter(Boolean).join(" ").toLowerCase(), value: String(id) }; }) .filter(Boolean); return [{ label: "全部人员", searchText: "all 全部人员", value: SOCIAL_BI_ALL_VALUE }, ...dedupeOptions(options)]; } function dedupeOptions(options) { const seen = new Set(); return options.filter((option) => { const key = String(option.value || option.label || "").trim().toLowerCase(); if (!key || seen.has(key)) { return false; } seen.add(key); return true; }); } function mergeAppOptions(options) { const seen = new Set(); return options.filter((option) => { const code = normalizeAppCode(option.code ?? option.value); if (seen.has(code)) { return false; } seen.add(code); return true; }); } function normalizeList(value) { if (Array.isArray(value)) { return value; } if (Array.isArray(value?.items)) { return value.items; } if (Array.isArray(value?.list)) { return value.list; } return []; } function normalizeCodes(value) { return Array.isArray(value) ? value.map((item) => String(item || "").trim().toUpperCase()).filter(Boolean) : []; } function buildCountryRegionIndex(regions) { const index = new Map(); regions.forEach((region) => { if (region.id === "all") { return; } region.countryCodes.forEach((code) => { const ids = index.get(code) || []; ids.push(region.id); index.set(code, ids); }); }); return index; } function normalizeCountryOption(country, countryRegionIds) { const countryCode = String(country.countryCode ?? country.country_code ?? country.code ?? "").trim().toUpperCase(); const id = Number(country.countryId ?? country.country_id ?? country.id ?? 0); if (!countryCode && !id) { return null; } const rawName = country.countryDisplayName || country.country_display_name || country.countryName || country.country_name || country.name || countryCode; const name = stripCountryFlagPrefix(rawName); const flag = country.flag || countryFlag(countryCode); const label = [flag, name].filter(Boolean).join(" "); const searchText = [flag, rawName, name, countryCode, country.isoAlpha3, country.iso_alpha3, country.phoneCountryCode, country.phone_country_code] .filter(Boolean) .join(" ") .toLowerCase(); return { countryCode, flag, id, label, name, regionIds: countryRegionIds.get(countryCode) || [], searchText, value: id }; }