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