335 lines
15 KiB
JavaScript

import { API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints";
import { validatePublicAppJumpParam } from "@/shared/app-jump/appJumpConfig.js";
import { externalRequest } from "./client.js";
import { asRecord, booleanValue, entityId, normalizePage, numberValue, textValue } from "./normalizers.js";
import { externalUiError } from "../i18n/errors.js";
// my-team is external-session scoped and is not part of the main-admin OpenAPI contract yet.
const EXTERNAL_ONLY_PATHS = Object.freeze({ myTeamAgencies: "/admin/my-team/agencies" });
export function listUsers(query) {
return externalRequest(externalOperationPath(API_OPERATIONS.appListUsers), { query }).then((data) => normalizePage(data, normalizeUser));
}
export function listBannedUsers(query) {
return externalRequest(externalOperationPath(API_OPERATIONS.appListBannedUsers), { query }).then((data) => normalizePage(data, normalizeBannedUser));
}
export function updateUser(userId, payload) {
return externalRequest(externalOperationPath(API_OPERATIONS.appUpdateUser, { id: userId }), { body: payload, method: "PATCH" });
}
export function banUser(userId, payload) {
return externalRequest(externalOperationPath(API_OPERATIONS.appBanUser, { id: userId }), { body: payload, method: "POST" });
}
export function unbanUser(userId, payload = {}) {
return externalRequest(externalOperationPath(API_OPERATIONS.appUnbanUser, { id: userId }), { body: payload, method: "POST" });
}
export function updateUserLevels(userId, payload) {
return externalRequest(externalOperationPath(API_OPERATIONS.appAdjustUserLevels, { id: userId }), { body: payload, method: "PUT" });
}
export async function grantUserVip(targetUserId, payload) {
const [programData, levelsData] = await Promise.all([
externalRequest(externalOperationPath(API_OPERATIONS.getVipProgram)),
externalRequest(externalOperationPath(API_OPERATIONS.getVipConfig))
]);
const programRoot = asRecord(programData);
const program = asRecord(programRoot.config || programRoot.programConfig || programRoot.program_config || programRoot);
const levelsRoot = asRecord(levelsData);
const levels = Array.isArray(levelsData) ? levelsData : levelsRoot.levels || [];
const level = Number(payload.level);
if (program.status === "disabled") {
throw externalUiError("errors.vipDisabled");
}
if (Array.isArray(levels) && levels.length && !levels.some((item) => Number(item.level) === level)) {
throw externalUiError("errors.vipLevelInvalid");
}
const command = {
commandId: payload.commandId,
level,
reason: payload.reason,
targetUserId
};
if (program.grantMode === "trial_card" || program.grant_mode === "trial_card") {
return externalRequest(externalOperationPath(API_OPERATIONS.grantVipTrialCard), {
body: { ...command, durationMs: Number(payload.durationMs) },
method: "POST"
});
}
return externalRequest(externalOperationPath(API_OPERATIONS.grantVip), { body: command, method: "POST" });
}
export function listOrganizations(kind, query = {}) {
const routes = {
agencies: externalOperationPath(API_OPERATIONS.listAgencies),
bds: externalOperationPath(API_OPERATIONS.listBDs),
"bd-leaders": externalOperationPath(API_OPERATIONS.listManagers),
hosts: externalOperationPath(API_OPERATIONS.listHosts),
managers: externalOperationPath(API_OPERATIONS.listManagers),
"super-admins": externalOperationPath(API_OPERATIONS.listBDLeaders),
team: EXTERNAL_ONLY_PATHS.myTeamAgencies
};
if (!routes[kind]) {
throw externalUiError("errors.organizationRoute");
}
const scopedQuery = kind === "super-admins" ? { ...query, status: query.status || "active" } : query;
return externalRequest(routes[kind], { query: scopedQuery }).then((data) => {
const page = normalizePage(data, normalizeOrganization);
if (kind !== "team") {
return page;
}
const source = asRecord(data);
return {
...page,
totalBdLeaders: numberValue(source.totalBdLeaders ?? source.total_bd_leaders),
totalBds: numberValue(source.totalBds ?? source.total_bds),
truncated: booleanValue(source.truncated)
};
});
}
export function listRooms(query) {
return externalRequest(externalOperationPath(API_OPERATIONS.listRooms), { query }).then((data) => normalizePage(data, normalizeRoom));
}
export function updateRoom(roomId, payload) {
return externalRequest(externalOperationPath(API_OPERATIONS.updateRoom, { room_id: roomId }), { body: buildRoomUpdatePayload(payload), method: "PATCH" });
}
export function buildRoomUpdatePayload(payload) {
const regionText = String(payload.visibleRegionId ?? "").trim();
const visibleRegionId = regionText ? Number(regionText) : 0;
if (!Number.isSafeInteger(visibleRegionId) || visibleRegionId < 0) {
throw externalUiError("errors.regionId");
}
return {
coverUrl: String(payload.coverUrl || "").trim(),
description: String(payload.description || "").trim(),
title: String(payload.title || "").trim(),
visibleRegionId
};
}
export function listResources(query) {
return externalRequest(externalOperationPath(API_OPERATIONS.listResources), { query }).then((data) => normalizePage(data, normalizeResource));
}
export function listResourceGrants(query) {
return externalRequest(externalOperationPath(API_OPERATIONS.listResourceGrants), { query }).then((data) => normalizePage(data));
}
export async function lookupGrantTarget(userIdOrPrettyId) {
const data = await externalRequest(externalOperationPath(API_OPERATIONS.lookupResourceGrantTarget), { query: { user_id: userIdOrPrettyId } });
const source = asRecord(data);
const user = asRecord(source.user || source.targetUser || source.target_user);
return {
id: textValue(source.targetUserId, source.target_user_id, source.userId, source.user_id, user.id, user.userId, user.user_id),
prettyId: textValue(source.prettyId, source.pretty_id, user.prettyId, user.pretty_id, user.displayUserId, user.display_user_id),
username: textValue(source.username, source.nickname, user.username, user.nickname)
};
}
export function grantResource(payload) {
return externalRequest(externalOperationPath(API_OPERATIONS.grantResource), { body: payload, method: "POST" });
}
export function grantPrettyId(payload) {
return externalRequest(externalOperationPath(API_OPERATIONS.grantPrettyId), { body: payload, method: "POST" });
}
export function listBanners(query) {
return externalRequest(externalOperationPath(API_OPERATIONS.listBanners), { query }).then((data) => normalizePage(data, normalizeBanner));
}
export function createBanner(payload) {
return externalRequest(externalOperationPath(API_OPERATIONS.createBanner), { body: buildBannerPayload(payload), method: "POST" });
}
export function buildBannerPayload(payload) {
if (!["h5", "app"].includes(payload.bannerType)) {
throw externalUiError("errors.bannerType");
}
if (!["android", "ios"].includes(payload.platform)) {
throw externalUiError("errors.bannerPlatform");
}
if (!["active", "disabled", "expired"].includes(payload.status)) {
throw externalUiError("errors.bannerStatus");
}
const displayScopes = [...new Set((payload.displayScopes || []).filter((scope) => ["home", "room", "recharge", "me"].includes(scope)))];
if (!displayScopes.length) {
throw externalUiError("errors.bannerScope");
}
if (!payload.coverUrl) {
throw externalUiError("errors.bannerImage");
}
if (payload.bannerType === "h5") {
let target;
try {
target = new URL(payload.param);
} catch {
throw externalUiError("errors.h5Url");
}
if (!["http:", "https:"].includes(target.protocol)) {
throw externalUiError("errors.h5Protocol");
}
}
if (payload.bannerType === "app") {
const validation = validatePublicAppJumpParam(payload.param);
if (!validation.valid) {
throw externalUiError(appJumpValidationKey(validation.message));
}
}
if (displayScopes.includes("room") && !payload.roomSmallImageUrl) {
throw externalUiError("errors.roomBannerImage");
}
const countryCode = String(payload.countryCode || "").trim().toUpperCase();
if (countryCode && !/^[A-Z]{2,3}$/.test(countryCode)) {
throw externalUiError("errors.countryCode");
}
const regionId = Number(payload.regionId || 0);
if (!Number.isSafeInteger(regionId) || regionId < 0) {
throw externalUiError("errors.bannerRegion");
}
const startsAtMs = Number(payload.startsAtMs || 0);
const endsAtMs = Number(payload.endsAtMs || 0);
if (startsAtMs > 0 && endsAtMs > 0 && startsAtMs >= endsAtMs) {
throw externalUiError("errors.bannerSchedule");
}
return {
bannerType: payload.bannerType,
countryCode,
coverUrl: payload.coverUrl,
description: String(payload.description || "").trim(),
displayScope: displayScopes[0],
displayScopes,
endsAtMs,
param: String(payload.param || "").trim(),
platform: payload.platform,
regionId,
roomSmallImageUrl: displayScopes.includes("room") ? payload.roomSmallImageUrl : "",
sortOrder: Number(payload.sortOrder || 0),
startsAtMs,
status: payload.status
};
}
export async function uploadExternalImage(file) {
const body = new FormData();
body.append("file", file);
const data = await externalRequest(externalOperationPath(API_OPERATIONS.uploadImage), { body, method: "POST" });
const source = asRecord(data);
return textValue(source.url, source.fileUrl, source.file_url, asRecord(source.file).url);
}
export function normalizeUser(item) {
const levels = asRecord(item.levels);
const wealth = asRecord(levels.wealth);
const charm = asRecord(levels.charm);
const vip = asRecord(item.vip);
const ban = asRecord(item.ban);
return {
avatar: textValue(item.avatar, item.avatarUrl, item.avatar_url),
banned: booleanValue(ban.active, item.banned, item.isBanned, item.is_banned) || ["banned", "disabled"].includes(String(item.status).toLowerCase()),
charmLevel: numberValue(item.charmLevel ?? item.charm_level ?? charm.displayLevel ?? charm.display_level ?? charm.level),
country: textValue(item.country, item.countryCode, item.country_code),
gender: textValue(item.gender),
id: entityId(item),
prettyId: textValue(item.prettyId, item.pretty_id, item.displayUserId, item.display_user_id, item.shortId, item.short_id),
status: textValue(item.status, item.banned ? "banned" : "active"),
username: textValue(item.username, item.nickname, item.nickName, item.name),
vipLevel: numberValue(item.vipLevel ?? item.vip_level ?? vip.level),
wealthLevel: numberValue(item.wealthLevel ?? item.wealth_level ?? wealth.displayLevel ?? wealth.display_level ?? wealth.level)
};
}
export function normalizeBannedUser(item) {
// Ban-list rows use the ban-log id as `id`; unban must always target the nested App user id instead.
const target = asRecord(item.target || item.user || item.targetUser || item.target_user);
const user = normalizeUser(target);
return {
...user,
banned: true,
bannedAtMs: numberValue(item.bannedAtMs ?? item.banned_at_ms ?? item.createdAtMs ?? item.created_at_ms),
banLogId: textValue(item.id, item.banLogId, item.ban_log_id),
expiresAtMs: numberValue(item.expiresAtMs ?? item.expires_at_ms),
id: textValue(target.userId, target.user_id, target.id),
prettyId: textValue(target.displayUserId, target.display_user_id, target.prettyId, target.pretty_id),
reason: textValue(item.reason)
};
}
export function normalizeOrganization(item) {
const user = asRecord(item.user || item.profile || item.manager);
const agency = Boolean(item.agencyId ?? item.agency_id);
return {
avatar: textValue(agency ? item.ownerAvatar : "", agency ? item.owner_avatar : "", item.avatar, item.avatarUrl, item.avatar_url, user.avatar),
appCode: textValue(item.appCode, item.app_code),
country: textValue(item.regionName, item.region_name, item.country, item.countryCode, item.country_code, user.regionName, user.region_name, user.country),
id: textValue(item.agencyId, item.agency_id, item.userId, item.user_id, item.id, entityId(user)),
name: textValue(item.name, agency ? item.ownerUsername : "", item.displayName, item.display_name, item.username, user.username, user.name),
parentBdUserId: textValue(item.parentBdUserId, item.parent_bd_user_id),
prettyId: textValue(agency ? item.ownerDisplayUserId : "", agency ? item.owner_display_user_id : "", item.displayUserId, item.display_user_id, item.prettyId, item.pretty_id, user.displayUserId, user.display_user_id, user.prettyId, user.pretty_id),
status: textValue(item.status, item.enabled === false ? "disabled" : "active"),
username: textValue(agency ? item.ownerUsername : "", agency ? item.owner_username : "", item.username, user.username, user.nickname),
userId: textValue(agency ? item.ownerUserId : "", agency ? item.owner_user_id : "", item.userId, item.user_id, user.userId, user.user_id)
};
}
function normalizeRoom(item) {
return {
coverUrl: textValue(item.coverUrl, item.cover_url, item.backgroundUrl, item.background_url),
description: textValue(item.description, item.introduction),
id: entityId(item),
ownerName: textValue(item.ownerName, item.owner_name, asRecord(item.owner).username),
status: textValue(item.status, item.liveStatus, item.live_status),
title: textValue(item.title, item.name, item.roomName, item.room_name),
visibleRegionId: textValue(item.visibleRegionId, item.visible_region_id)
};
}
function normalizeResource(item) {
return {
coverUrl: textValue(item.coverUrl, item.cover_url, item.iconUrl, item.icon_url),
id: textValue(item.resourceId, item.resource_id, item.id),
name: textValue(item.name, item.resourceName, item.resource_name, item.resourceCode, item.resource_code),
status: textValue(item.status),
type: textValue(item.resourceType, item.resource_type, item.type)
};
}
function normalizeBanner(item) {
return {
bannerType: textValue(item.bannerType, item.banner_type, item.type),
coverUrl: textValue(item.coverUrl, item.cover_url, item.imageUrl, item.image_url),
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
description: textValue(item.description, item.title),
id: textValue(item.id, item.bannerId, item.banner_id),
platform: textValue(item.platform),
sortOrder: numberValue(item.sortOrder ?? item.sort_order),
status: textValue(item.status)
};
}
function externalOperationPath(operation, params) {
// Generated main-admin paths start with /v1; the external gateway already owns that version prefix in its base URL.
return apiEndpointPath(operation, params).replace(/^\/v1(?=\/)/, "");
}
function appJumpValidationKey(message) {
return {
"APP 跳转参数必须是 JSON 对象": "errors.appParamObject",
"APP 跳转 type 不在公共跳转方案内": "errors.appParamType",
"Explore 游戏 H5 需要选择有效 game_code": "errors.appParamGameCode",
"该 APP 跳转需要填写 game_id": "errors.appParamGame",
"该 APP 跳转需要填写 room_id": "errors.appParamRoom",
"该 APP 跳转需要填写有效 window": "errors.appParamWindow",
"请填写 APP 跳转参数": "errors.appParamRequired"
}[message] || "errors.appParamObject";
}