502 lines
15 KiB
JavaScript
502 lines
15 KiB
JavaScript
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
|
||
};
|
||
}
|