2026-07-12 00:47:34 +08:00

502 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
};
}