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