diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 2fbd957..a0ce04d 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -2637,6 +2637,40 @@ "x-permissions": ["payment-product:delete"] } }, + "/admin/payment/temporary-links": { + "get": { + "operationId": "listTemporaryPaymentLinks", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "payment-temporary-link:view", + "x-permissions": ["payment-temporary-link:view"] + } + }, + "/admin/payment/temporary-links/{order_id}": { + "get": { + "operationId": "getTemporaryPaymentLink", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "order_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "x-permission": "payment-temporary-link:view", + "x-permissions": ["payment-temporary-link:view"] + } + }, "/admin/payment/third-party-channels": { "get": { "operationId": "listThirdPartyPaymentChannels", @@ -3612,6 +3646,44 @@ "x-permissions": ["region-block:update"] } }, + "/admin/users/risk-config": { + "get": { + "operationId": "getRiskConfig", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "risk-config:view", + "x-permissions": ["risk-config:view"] + }, + "put": { + "operationId": "updateRiskConfig", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["max_accounts_per_device"], + "properties": { + "max_accounts_per_device": { + "type": "integer", + "minimum": 1 + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "risk-config:update", + "x-permissions": ["risk-config:update"] + } + }, "/app/users": { "get": { "operationId": "appListUsers", diff --git a/src/app/navigation/menu.js b/src/app/navigation/menu.js index 165722a..d2e205f 100644 --- a/src/app/navigation/menu.js +++ b/src/app/navigation/menu.js @@ -115,6 +115,7 @@ export const fallbackNavigation = [ routeNavItem("app-user-bans", { icon: BlockOutlined }), routeNavItem("app-user-login-logs", { icon: LoginOutlined }), routeNavItem("app-user-pretty-ids", { icon: LocalOfferOutlined }), + routeNavItem("app-user-risk-config", { icon: ShieldOutlined }), routeNavItem("app-user-region-blocks", { icon: PublicOutlined }), ], }, @@ -198,6 +199,7 @@ export const fallbackNavigation = [ children: [ routeNavItem("payment-bill-list", { icon: ReceiptLongOutlined }), routeNavItem("payment-third-party", { icon: WalletOutlined }), + routeNavItem("payment-temporary-links", { icon: ReceiptLongOutlined }), routeNavItem("payment-recharge-products", { icon: WalletOutlined }), ], }, diff --git a/src/app/permissions.ts b/src/app/permissions.ts index 6b73219..5cdfe16 100644 --- a/src/app/permissions.ts +++ b/src/app/permissions.ts @@ -52,6 +52,7 @@ export const PERMISSIONS = { paymentProductCreate: "payment-product:create", paymentProductUpdate: "payment-product:update", paymentProductDelete: "payment-product:delete", + paymentTemporaryLinkView: "payment-temporary-link:view", paymentThirdPartyView: "payment-third-party:view", paymentThirdPartyUpdate: "payment-third-party:update", gameView: "game:view", @@ -89,6 +90,8 @@ export const PERMISSIONS = { prettyIdGrant: "pretty-id:grant", levelConfigView: "level-config:view", levelConfigUpdate: "level-config:update", + riskConfigView: "risk-config:view", + riskConfigUpdate: "risk-config:update", regionBlockView: "region-block:view", regionBlockUpdate: "region-block:update", roomView: "room:view", @@ -190,6 +193,7 @@ export const MENU_CODES = { appUserBans: "app-user-bans", appUserLoginLogs: "app-user-login-logs", appUserLevelConfig: "app-user-level-config", + appUserRiskConfig: "app-user-risk-config", appUserRegionBlocks: "app-user-region-blocks", appUserPrettyIds: "app-user-pretty-ids", rooms: "rooms", @@ -252,6 +256,7 @@ export const MENU_CODES = { payment: "payment", paymentBillList: "payment-bill-list", paymentRechargeProducts: "payment-recharge-products", + paymentTemporaryLinks: "payment-temporary-links", paymentThirdParty: "payment-third-party", games: "games", gameList: "game-list", diff --git a/src/app/router/routeConfig.ts b/src/app/router/routeConfig.ts index d182710..c88b43c 100644 --- a/src/app/router/routeConfig.ts +++ b/src/app/router/routeConfig.ts @@ -22,6 +22,7 @@ import { registrationRewardRoutes } from "@/features/registration-reward/routes. import { redPacketRoutes } from "@/features/red-packets/routes.js"; import { regionBlockRoutes } from "@/features/region-blocks/routes.js"; import { resourceRoutes } from "@/features/resources/routes.js"; +import { riskConfigRoutes } from "@/features/risk-config/routes.js"; import { rolesRoutes } from "@/features/roles/routes.js"; import { roomRoutes } from "@/features/rooms/routes.js"; import { roomRocketRoutes } from "@/features/room-rocket/routes.js"; @@ -40,6 +41,7 @@ export const adminRoutes: AdminRoute[] = [ ...dashboardRoutes, ...appUserRoutes, ...levelConfigRoutes, + ...riskConfigRoutes, ...regionBlockRoutes, ...roomRoutes, ...appConfigRoutes, diff --git a/src/features/app-users/api.test.ts b/src/features/app-users/api.test.ts index be07f08..a88042a 100644 --- a/src/features/app-users/api.test.ts +++ b/src/features/app-users/api.test.ts @@ -106,6 +106,12 @@ test("listAppUsers normalizes pretty display id fields from snake case", async ( pretty_id: "pretty-2026", user_id: "10001", username: "tester", + vip: { + active: true, + expires_at_ms: 1790000000000, + level: 3, + name: "黄金会员", + }, }, ], page: 1, @@ -127,10 +133,15 @@ test("listAppUsers normalizes pretty display id fields from snake case", async ( prettyId: "pretty-2026", userId: "10001", username: "tester", + vip: expect.objectContaining({ + active: true, + level: 3, + name: "黄金会员", + }), }); }); -test("getAppUser uses detail endpoint and normalizes pretty display id", async () => { +test("getAppUser uses detail endpoint and normalizes assets", async () => { vi.stubGlobal( "fetch", vi.fn( @@ -144,6 +155,39 @@ test("getAppUser uses detail endpoint and normalizes pretty display id", async ( pretty_display_user_id: "VIP888", pretty_id: "pretty-888", user_id: "10001", + balances: [ + { + asset_type: "COIN", + available_amount: 1200, + frozen_amount: 50, + total_amount: 1250, + version: 7, + }, + ], + equipped_resources: [ + { + entitlement_id: "ent-1", + equipped: true, + name: "金色头像框", + quantity: 1, + remaining_quantity: 1, + resource_code: "frame_gold", + resource_id: 9, + resource_type: "avatar_frame", + }, + ], + resources: [ + { + entitlement_id: "ent-1", + equipped: true, + name: "金色头像框", + quantity: 1, + remaining_quantity: 1, + resource_code: "frame_gold", + resource_id: 9, + resource_type: "avatar_frame", + }, + ], }, }), ), @@ -158,4 +202,20 @@ test("getAppUser uses detail endpoint and normalizes pretty display id", async ( expect(result.defaultDisplayUserId).toBe("123456"); expect(result.prettyDisplayUserId).toBe("VIP888"); expect(result.prettyId).toBe("pretty-888"); + expect(result.balances?.[0]).toMatchObject({ + assetType: "COIN", + availableAmount: 1200, + frozenAmount: 50, + totalAmount: 1250, + version: 7, + }); + expect(result.equippedResources?.[0]).toMatchObject({ + equipped: true, + name: "金色头像框", + resourceType: "avatar_frame", + }); + expect(result.resources?.[0]).toMatchObject({ + entitlementId: "ent-1", + resourceCode: "frame_gold", + }); }); diff --git a/src/features/app-users/api.ts b/src/features/app-users/api.ts index c3a5871..4804b88 100644 --- a/src/features/app-users/api.ts +++ b/src/features/app-users/api.ts @@ -198,6 +198,7 @@ export async function setPrettyDisplayIDStatus( // 用户相关接口历史上既返回过 snake_case,也返回过 camelCase;在 API 边界归一化后,共享用户组件只消费稳定字段。 interface RawAppUser { avatar?: string; + balances?: RawAppUserAssetBalance[]; coin?: number; country?: string; country_display_name?: string; @@ -211,6 +212,8 @@ interface RawAppUser { diamond?: number; display_user_id?: string; displayUserId?: string; + equipped_resources?: RawAppUserResource[]; + equippedResources?: RawAppUserResource[]; gender?: string; last_active_at_ms?: number; lastActiveAtMs?: number; @@ -222,16 +225,77 @@ interface RawAppUser { regionId?: number; region_name?: string; regionName?: string; + resources?: RawAppUserResource[]; status?: string; updated_at_ms?: number; updatedAtMs?: number; user_id?: EntityId; userId?: EntityId; username?: string; + vip?: RawAppUserVIP; } type RawAppUserBrief = RawAppUser; +interface RawAppUserAssetBalance { + asset_type?: string; + assetType?: string; + available_amount?: number; + availableAmount?: number; + frozen_amount?: number; + frozenAmount?: number; + total_amount?: number; + totalAmount?: number; + updated_at_ms?: number; + updatedAtMs?: number; + version?: number; +} + +interface RawAppUserVIP { + active?: boolean; + expires_at_ms?: number; + expiresAtMs?: number; + level?: number; + name?: string; + started_at_ms?: number; + startedAtMs?: number; + updated_at_ms?: number; + updatedAtMs?: number; +} + +interface RawAppUserResource { + animation_url?: string; + animationUrl?: string; + asset_url?: string; + assetUrl?: string; + created_at_ms?: number; + createdAtMs?: number; + effective_at_ms?: number; + effectiveAtMs?: number; + entitlement_id?: string; + entitlementId?: string; + equipped?: boolean; + expires_at_ms?: number; + expiresAtMs?: number; + name?: string; + preview_url?: string; + previewUrl?: string; + quantity?: number; + remaining_quantity?: number; + remainingQuantity?: number; + resource_code?: string; + resourceCode?: string; + resource_id?: number; + resourceId?: number; + resource_type?: string; + resourceType?: string; + source_grant_id?: string; + sourceGrantId?: string; + status?: string; + updated_at_ms?: number; + updatedAtMs?: number; +} + interface RawAppUserBanOperator extends RawAppUserBrief { account?: string; admin_id?: EntityId; @@ -410,6 +474,7 @@ function normalizePage( function normalizeAppUser(item: RawAppUser = {}): AppUserDto { return { avatar: stringValue(item.avatar), + balances: (item.balances || []).map(normalizeAppUserAssetBalance), coin: numberValue(item.coin), country: stringValue(item.country), countryDisplayName: stringValue(item.countryDisplayName ?? item.country_display_name), @@ -418,16 +483,65 @@ function normalizeAppUser(item: RawAppUser = {}): AppUserDto { defaultDisplayUserId: stringValue(item.defaultDisplayUserId ?? item.default_display_user_id), diamond: numberValue(item.diamond), displayUserId: stringValue(item.displayUserId ?? item.display_user_id), + equippedResources: (item.equippedResources ?? item.equipped_resources ?? []).map(normalizeAppUserResource), gender: stringValue(item.gender), lastActiveAtMs: numberValue(item.lastActiveAtMs ?? item.last_active_at_ms), prettyDisplayUserId: stringValue(item.prettyDisplayUserId ?? item.pretty_display_user_id), prettyId: stringValue(item.prettyId ?? item.pretty_id), regionId: numberValue(item.regionId ?? item.region_id), regionName: stringValue(item.regionName ?? item.region_name), + resources: (item.resources || []).map(normalizeAppUserResource), status: stringValue(item.status), updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms), userId: stringValue(item.userId ?? item.user_id), username: stringValue(item.username), + vip: normalizeAppUserVIP(item.vip), + }; +} + +function normalizeAppUserAssetBalance(item: RawAppUserAssetBalance = {}) { + const availableAmount = numberValue(item.availableAmount ?? item.available_amount); + const frozenAmount = numberValue(item.frozenAmount ?? item.frozen_amount); + return { + assetType: stringValue(item.assetType ?? item.asset_type).toUpperCase(), + availableAmount, + frozenAmount, + totalAmount: numberValue(item.totalAmount ?? item.total_amount ?? availableAmount + frozenAmount), + updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms), + version: numberValue(item.version), + }; +} + +function normalizeAppUserVIP(item: RawAppUserVIP = {}) { + return { + active: Boolean(item.active), + expiresAtMs: numberValue(item.expiresAtMs ?? item.expires_at_ms), + level: numberValue(item.level), + name: stringValue(item.name), + startedAtMs: numberValue(item.startedAtMs ?? item.started_at_ms), + updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms), + }; +} + +function normalizeAppUserResource(item: RawAppUserResource = {}) { + return { + animationUrl: stringValue(item.animationUrl ?? item.animation_url), + assetUrl: stringValue(item.assetUrl ?? item.asset_url), + createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms), + effectiveAtMs: numberValue(item.effectiveAtMs ?? item.effective_at_ms), + entitlementId: stringValue(item.entitlementId ?? item.entitlement_id), + equipped: Boolean(item.equipped), + expiresAtMs: numberValue(item.expiresAtMs ?? item.expires_at_ms), + name: stringValue(item.name), + previewUrl: stringValue(item.previewUrl ?? item.preview_url), + quantity: numberValue(item.quantity), + remainingQuantity: numberValue(item.remainingQuantity ?? item.remaining_quantity), + resourceCode: stringValue(item.resourceCode ?? item.resource_code), + resourceId: numberValue(item.resourceId ?? item.resource_id), + resourceType: stringValue(item.resourceType ?? item.resource_type), + sourceGrantId: stringValue(item.sourceGrantId ?? item.source_grant_id), + status: stringValue(item.status), + updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms), }; } diff --git a/src/features/app-users/app-users.module.css b/src/features/app-users/app-users.module.css index 3e07e7a..32d6538 100644 --- a/src/features/app-users/app-users.module.css +++ b/src/features/app-users/app-users.module.css @@ -441,6 +441,155 @@ font-weight: 650; } +.vipBadge, +.vipBadgeMuted { + display: inline-flex; + width: fit-content; + align-items: center; + justify-content: center; + padding: 3px 8px; + border-radius: var(--radius-sm); + font-size: 12px; + font-weight: 800; + line-height: 1.2; +} + +.vipBadge { + border: 1px solid color-mix(in srgb, #f2b84b 55%, var(--border)); + background: #fff7df; + color: #8a5a00; +} + +.vipBadgeMuted { + border: 1px solid var(--border); + background: var(--neutral-surface); + color: var(--text-tertiary); +} + +.emptyInline { + display: flex; + min-height: 40px; + align-items: center; + color: var(--text-tertiary); + font-size: var(--admin-font-size); + font-weight: 650; +} + +.assetList { + display: grid; + gap: var(--space-2); +} + +.assetRow { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-card); +} + +.assetName { + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 800; +} + +.assetMeta, +.resourceMeta { + color: var(--text-tertiary); + font-size: 12px; + line-height: 1.35; +} + +.assetAmount { + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 800; + text-align: right; +} + +.resourceList { + display: grid; + gap: var(--space-2); + padding: 0; + margin: 0; + list-style: none; +} + +.resourceItem { + display: grid; + grid-template-columns: 44px minmax(0, 1fr); + gap: var(--space-3); + align-items: center; + min-width: 0; + padding: var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-card); +} + +.resourceThumb, +.resourceThumbFallback { + width: 44px; + height: 44px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--neutral-surface); +} + +.resourceThumb { + display: block; + object-fit: cover; +} + +.resourceThumbFallback { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); + font-size: 16px; + font-weight: 800; +} + +.resourceBody { + display: grid; + min-width: 0; + gap: 3px; +} + +.resourceTitle { + display: flex; + min-width: 0; + align-items: center; + gap: var(--space-2); + color: var(--text-primary); + font-size: var(--admin-font-size); + font-weight: 800; +} + +.resourceTitle span:first-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.resourceTag { + flex: 0 0 auto; + padding: 2px 7px; + border: 1px solid var(--success-border); + border-radius: var(--radius-sm); + background: var(--success-surface); + color: var(--success); + font-size: 11px; + font-weight: 800; + line-height: 1.2; +} + .stack { display: grid; gap: 1px; diff --git a/src/features/app-users/components/AppUserDetailProvider.jsx b/src/features/app-users/components/AppUserDetailProvider.jsx index a60215b..30a1e4d 100644 --- a/src/features/app-users/components/AppUserDetailProvider.jsx +++ b/src/features/app-users/components/AppUserDetailProvider.jsx @@ -70,6 +70,9 @@ function AppUserDetailDrawer({ loading, onClose, open, user }) { if (!open || !user) { return null; } + const balances = user.balances || []; + const equippedResources = user.equippedResources || []; + const resources = user.resources || []; return ( @@ -93,6 +96,7 @@ function AppUserDetailDrawer({ loading, onClose, open, user }) {

账号状态

+ } /> @@ -105,6 +109,18 @@ function AppUserDetailDrawer({ loading, onClose, open, user }) {
+
+

资产余额

+ +
+
+

当前佩戴

+ +
+
+

资源资产

+ +
); } @@ -128,6 +144,67 @@ function PrettyValue({ user }) { return ; } +function VipDetailValue({ vip }) { + const level = Number(vip?.level || 0); + if (level <= 0 || !vip?.active) { + return "VIP 0"; + } + return vip.name ? `VIP ${level} · ${vip.name}` : `VIP ${level}`; +} + +function AssetBalanceList({ balances }) { + if (!balances.length) { + return
当前无数据
; + } + return ( +
+ {balances.map((balance) => ( +
+
+
{formatAssetType(balance.assetType)}
+
冻结 {formatNumber(balance.frozenAmount)}
+
+
{formatNumber(balance.availableAmount)}
+
+ ))} +
+ ); +} + +function ResourceList({ resources }) { + if (!resources.length) { + return
当前无数据
; + } + return ( + + ); +} + +function ResourceThumb({ resource }) { + const imageUrl = resource.previewUrl || resource.assetUrl || resource.animationUrl; + if (!imageUrl) { + return {resource.name?.slice(0, 1) || "资"}; + } + return ; +} + function AppUserStatus({ status }) { const tone = status === "active" ? "running" : "danger"; return ( @@ -190,6 +267,39 @@ function formatNumber(value) { return Number.isFinite(number) ? number.toLocaleString() : "-"; } +function formatAssetType(value) { + const normalized = String(value || "").toUpperCase(); + const labels = { + COIN: "金币", + DIAMOND: "钻石", + }; + return labels[normalized] || normalized || "-"; +} + +function formatResourceType(value) { + const labels = { + avatar_frame: "头像框", + badge: "徽章", + chat_bubble: "聊天气泡", + emoji_pack: "表情包", + floating_screen: "飘屏", + gift: "礼物", + mic_seat_animation: "麦位动画", + mic_seat_icon: "麦位图标", + profile_card: "资料卡", + vehicle: "座驾", + }; + return labels[value] || value || "-"; +} + +function formatExpireTime(value) { + const expiresAtMs = Number(value || 0); + if (!Number.isFinite(expiresAtMs) || expiresAtMs <= 0) { + return "永久"; + } + return formatMillis(expiresAtMs); +} + function firstNonEmpty(...values) { for (const value of values) { const normalized = String(value ?? "").trim(); diff --git a/src/features/app-users/pages/AppUserListPage.jsx b/src/features/app-users/pages/AppUserListPage.jsx index 361ad6d..92e70d8 100644 --- a/src/features/app-users/pages/AppUserListPage.jsx +++ b/src/features/app-users/pages/AppUserListPage.jsx @@ -90,6 +90,12 @@ export function AppUserListPage() { }), render: (user) => , }, + { + key: "vip", + label: "VIP", + width: "minmax(80px, 0.45fr)", + render: (user) => , + }, { key: "location", label: "国家 / 区域", @@ -316,6 +322,14 @@ function CoinValue({ page, user }) { ); } +function VipValue({ vip }) { + const level = Number(vip?.level || 0); + if (level <= 0 || !vip?.active) { + return VIP 0; + } + return VIP {level}; +} + function UserLocation({ page, user }) { const countryLabel = formatCountry(user); const regionLabel = user.regionName || (user.regionId ? `区域 ${user.regionId}` : "-"); diff --git a/src/features/app-users/pages/AppUserListPage.test.jsx b/src/features/app-users/pages/AppUserListPage.test.jsx index c29aee4..acddfef 100644 --- a/src/features/app-users/pages/AppUserListPage.test.jsx +++ b/src/features/app-users/pages/AppUserListPage.test.jsx @@ -28,6 +28,7 @@ test("clicking an app user's coin opens the coin ledger drawer", () => { render(); + expect(screen.getByText("VIP 2")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "查看 d 的金币流水" })); expect(mocks.openCoinLedger).toHaveBeenCalledWith(expect.objectContaining({ userId: "10001" })); @@ -121,5 +122,6 @@ function userFixture() { status: "active", userId: "10001", username: "d", + vip: { active: true, level: 2, name: "白银会员" }, }; } diff --git a/src/features/payment/api.ts b/src/features/payment/api.ts index 8b2fb29..f159750 100644 --- a/src/features/payment/api.ts +++ b/src/features/payment/api.ts @@ -8,6 +8,7 @@ import type { RechargeBillDto, RechargeProductDto, RechargeProductPayload, + TemporaryPaymentLinkDto, ThirdPartyPaymentChannelDto, ThirdPartyPaymentMethodDto, ThirdPartyPaymentRateSyncDto, @@ -71,6 +72,27 @@ export function listThirdPartyPaymentChannels(): Promise> { + const endpoint = API_ENDPOINTS.listTemporaryPaymentLinks; + return apiRequest>( + apiEndpointPath(API_OPERATIONS.listTemporaryPaymentLinks), + { + method: endpoint.method, + query, + }, + ); +} + +export function getTemporaryPaymentLink(orderId: EntityId): Promise { + const endpoint = API_ENDPOINTS.getTemporaryPaymentLink; + return apiRequest( + apiEndpointPath(API_OPERATIONS.getTemporaryPaymentLink, { order_id: orderId }), + { + method: endpoint.method, + }, + ); +} + export function setThirdPartyPaymentMethodStatus( methodId: EntityId, payload: { enabled: boolean }, diff --git a/src/features/payment/hooks/useTemporaryPaymentLinksPage.js b/src/features/payment/hooks/useTemporaryPaymentLinksPage.js new file mode 100644 index 0000000..5a341ec --- /dev/null +++ b/src/features/payment/hooks/useTemporaryPaymentLinksPage.js @@ -0,0 +1,106 @@ +import { useMemo, useState } from "react"; +import { getTemporaryPaymentLink, listTemporaryPaymentLinks } from "@/features/payment/api"; +import { usePaymentAbilities } from "@/features/payment/permissions.js"; +import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; +import { useToast } from "@/shared/ui/ToastProvider.jsx"; + +const pageSize = 50; + +export function useTemporaryPaymentLinksPage() { + const abilities = usePaymentAbilities(); + const { showToast } = useToast(); + const [keyword, setKeyword] = useState(""); + const [status, setStatus] = useState(""); + const [providerCode, setProviderCode] = useState(""); + const [page, setPage] = useState(1); + const [loadingAction, setLoadingAction] = useState(""); + + const filters = useMemo( + () => ({ + keyword, + provider_code: providerCode, + status, + }), + [keyword, providerCode, status], + ); + + const result = usePaginatedQuery({ + errorMessage: "加载三方临时支付链接失败", + fetcher: listTemporaryPaymentLinks, + filters, + page, + pageSize, + queryKey: ["payment", "temporary-links", filters, page], + }); + + const resetPage = (setter) => (value) => { + setter(value); + setPage(1); + }; + + const resetFilters = () => { + setKeyword(""); + setStatus(""); + setProviderCode(""); + setPage(1); + }; + + const verifyOrder = async (orderId) => { + // 单条核验必须走 admin-server,由 wallet-service 按 provider 重新查询状态;前端只传订单号,避免三方签名参数外露。 + const id = String(orderId || "").trim(); + if (!id || !abilities.canViewTemporaryLink) { + return null; + } + setLoadingAction(`verify:${id}`); + try { + const order = await getTemporaryPaymentLink(id); + await result.reload(); + showToast(order?.status === "paid" ? "支付已成功" : `当前状态:${temporaryStatusLabel(order?.status)}`, "success"); + return order; + } catch (err) { + showToast(err.message || "验证支付状态失败", "error"); + return null; + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + data: result.data, + error: result.error, + keyword, + loading: result.loading, + loadingAction, + page, + pageSize, + providerCode, + reload: result.reload, + resetFilters, + setKeyword: resetPage(setKeyword), + setPage, + setProviderCode: resetPage(setProviderCode), + setStatus: resetPage(setStatus), + status, + verifyOrder, + }; +} + +export function temporaryStatusLabel(value) { + if (value === "paid") { + return "已支付"; + } + if (value === "redirected") { + return "已生成链接"; + } + if (value === "pending") { + return "待支付"; + } + if (value === "failed") { + return "失败"; + } + if (value === "credited") { + return "已入账"; + } + return value || "-"; +} diff --git a/src/features/payment/pages/TemporaryPaymentLinksPage.jsx b/src/features/payment/pages/TemporaryPaymentLinksPage.jsx new file mode 100644 index 0000000..2175cdc --- /dev/null +++ b/src/features/payment/pages/TemporaryPaymentLinksPage.jsx @@ -0,0 +1,303 @@ +import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined"; +import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; +import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined"; +import VerifiedOutlined from "@mui/icons-material/VerifiedOutlined"; +import { useMemo, useState } from "react"; +import { useTemporaryPaymentLinksPage, temporaryStatusLabel } from "@/features/payment/hooks/useTemporaryPaymentLinksPage.js"; +import styles from "@/features/payment/payment.module.css"; +import { + AdminActionIconButton, + AdminListBody, + AdminListPage, + AdminListToolbar, + AdminRowActions, +} from "@/shared/ui/AdminListLayout.jsx"; +import { Button } from "@/shared/ui/Button.jsx"; +import { DataState } from "@/shared/ui/DataState.jsx"; +import { DataTable } from "@/shared/ui/DataTable.jsx"; +import { StatusSelect } from "@/shared/ui/StatusSelect.jsx"; +import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; +import { formatMillis } from "@/shared/utils/time.js"; + +const statusOptions = [ + ["", "全部状态"], + ["pending", "待支付"], + ["redirected", "已生成链接"], + ["paid", "已支付"], + ["failed", "失败"], +]; + +const providerOptions = [ + ["", "全部渠道"], + ["mifapay", "MiFaPay"], + ["v5pay", "V5Pay"], +]; + +export function TemporaryPaymentLinksPage() { + const page = useTemporaryPaymentLinksPage(); + const items = page.data?.items || []; + const total = page.data?.total || 0; + const columns = useMemo(() => temporaryPaymentLinkColumns(page), [page]); + + return ( + + } onClick={page.resetFilters}> + 重置筛选 + + } + filters={ + <> + + + + } + /> + + + 0 + ? { + page: page.page, + pageSize: page.data?.pageSize || page.pageSize, + total, + onPageChange: page.setPage, + } + : undefined + } + rowKey={(item) => item.orderId} + /> + + + + ); +} + +function temporaryPaymentLinkColumns(page) { + return [ + { + key: "orderId", + label: "订单号", + width: "minmax(160px, 0.8fr)", + filter: createTextColumnFilter({ + placeholder: "搜索订单号、三方单号", + value: page.keyword, + onChange: page.setKeyword, + }), + render: (item) => , + }, + { + key: "provider", + label: "支付方式", + width: "minmax(170px, 0.85fr)", + filter: createOptionsColumnFilter({ + options: providerOptions, + placeholder: "搜索支付渠道", + value: page.providerCode, + onChange: page.setProviderCode, + }), + render: (item) => ( + + ), + }, + { + key: "amount", + label: "金币/美元", + width: "minmax(140px, 0.65fr)", + render: (item) => ( + + ), + }, + { + key: "providerAmount", + label: "三方金额", + width: "minmax(130px, 0.6fr)", + render: (item) => formatMoney(item.providerAmountMinor, item.currencyCode), + }, + { + key: "payUrl", + label: "支付链接", + width: "minmax(260px, 1.2fr)", + render: (item) => , + }, + { + key: "providerOrderId", + label: "三方订单", + width: "minmax(160px, 0.75fr)", + render: (item) => , + }, + { + key: "status", + label: "支付状态", + width: "minmax(110px, 0.55fr)", + filter: createOptionsColumnFilter({ + options: statusOptions, + placeholder: "搜索状态", + value: page.status, + onChange: page.setStatus, + }), + render: (item) => , + }, + { + key: "time", + label: "创建时间", + width: "minmax(160px, 0.75fr)", + render: (item) => , + }, + { + key: "actions", + label: "操作", + width: "128px", + fixed: "right", + resizable: false, + render: (item) => ( + + page.verifyOrder(item.orderId)} + > + + + openLink(item.payUrl)} + > + + + + ), + }, + ]; +} + +function PaymentLink({ value }) { + if (!value) { + return "-"; + } + return ( + + {value} + + + ); +} + +function CopyableText({ label, value }) { + if (!value) { + return "-"; + } + return ( + + {value} + + + ); +} + +function CopyButton({ label, value }) { + const [copied, setCopied] = useState(false); + const copyValue = async () => { + await copyText(String(value || "")); + setCopied(true); + window.setTimeout(() => setCopied(false), 1200); + }; + return ( + + + + + + ); +} + +function StatusBadge({ status }) { + const tone = + status === "paid" || status === "credited" ? "succeeded" : status === "failed" ? "danger" : "stopped"; + return ( + + + {temporaryStatusLabel(status)} + + ); +} + +function Stack({ primary, secondary }) { + return ( +
+ {primary || "-"} + {secondary || "-"} +
+ ); +} + +function providerLabel(value) { + return providerOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "-"; +} + +function formatNumber(value) { + if (value === 0 || value) { + return Number(value).toLocaleString("zh-CN"); + } + return "-"; +} + +function formatMoney(value, currency = "USD") { + if (!(value === 0 || value)) { + return "-"; + } + return `${currency || "USD"} ${(Number(value) / 100).toLocaleString("zh-CN", { + maximumFractionDigits: 2, + minimumFractionDigits: 2, + })}`; +} + +function openLink(value) { + if (!value) { + return; + } + window.open(value, "_blank", "noopener,noreferrer"); +} + +async function copyText(value) { + if (navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(value); + return; + } catch { + // 后台可能运行在非安全上下文,Clipboard API 被浏览器拒绝时退回同步复制。 + } + } + + const input = document.createElement("textarea"); + input.value = value; + input.setAttribute("readonly", ""); + input.style.position = "fixed"; + input.style.opacity = "0"; + document.body.appendChild(input); + input.select(); + document.execCommand("copy"); + input.remove(); +} diff --git a/src/features/payment/payment.module.css b/src/features/payment/payment.module.css index 1884f0c..2d79019 100644 --- a/src/features/payment/payment.module.css +++ b/src/features/payment/payment.module.css @@ -3,6 +3,23 @@ align-items: center; } +.copyableText, +.temporaryLinkCell { + display: grid; + min-width: 0; + align-items: center; + gap: var(--space-2); + grid-template-columns: minmax(0, 1fr) var(--control-height); +} + +.copyableText > span:first-child, +.temporaryLinkCell > span:first-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .identity { display: inline-flex; min-width: 0; diff --git a/src/features/payment/permissions.js b/src/features/payment/permissions.js index f2f7646..7a799c6 100644 --- a/src/features/payment/permissions.js +++ b/src/features/payment/permissions.js @@ -10,6 +10,7 @@ export function usePaymentAbilities() { canUpdateProduct: can(PERMISSIONS.paymentProductUpdate), canViewBill: can(PERMISSIONS.paymentBillView), canViewProduct: can(PERMISSIONS.paymentProductView), + canViewTemporaryLink: can(PERMISSIONS.paymentTemporaryLinkView), canUpdateThirdParty: can(PERMISSIONS.paymentThirdPartyUpdate), canViewThirdParty: can(PERMISSIONS.paymentThirdPartyView), }; diff --git a/src/features/payment/routes.js b/src/features/payment/routes.js index e38478c..992e45f 100644 --- a/src/features/payment/routes.js +++ b/src/features/payment/routes.js @@ -17,6 +17,15 @@ export const paymentRoutes = [ path: "/payment/third-party", permission: PERMISSIONS.paymentThirdPartyView, }, + { + label: "三方临时支付链接", + loader: () => + import("./pages/TemporaryPaymentLinksPage.jsx").then((module) => module.TemporaryPaymentLinksPage), + menuCode: MENU_CODES.paymentTemporaryLinks, + pageKey: "payment-temporary-links", + path: "/payment/temporary-links", + permission: PERMISSIONS.paymentTemporaryLinkView, + }, { label: "支付内购商品", loader: () => diff --git a/src/features/risk-config/api.ts b/src/features/risk-config/api.ts new file mode 100644 index 0000000..7d8da14 --- /dev/null +++ b/src/features/risk-config/api.ts @@ -0,0 +1,48 @@ +import { API_ENDPOINTS, API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints"; +import { apiRequest } from "@/shared/api/request"; + +export interface RiskConfigDto { + appCode: string; + maxAccountsPerDevice: number; + updatedAtMs: number; + updatedByAdminId: number; +} + +export interface RiskConfigPayload { + max_accounts_per_device: number; +} + +type RawRiskConfig = RiskConfigDto & Record; + +export function getRiskConfig(): Promise { + const endpoint = API_ENDPOINTS.getRiskConfig; + return apiRequest(apiEndpointPath(API_OPERATIONS.getRiskConfig), { + method: endpoint.method, + }).then(normalizeRiskConfig); +} + +export function updateRiskConfig(payload: RiskConfigPayload): Promise { + const endpoint = API_ENDPOINTS.updateRiskConfig; + return apiRequest(apiEndpointPath(API_OPERATIONS.updateRiskConfig), { + body: payload, + method: endpoint.method, + }).then(normalizeRiskConfig); +} + +function normalizeRiskConfig(data: RawRiskConfig): RiskConfigDto { + return { + appCode: stringValue(data.appCode ?? data.app_code), + maxAccountsPerDevice: numberValue(data.maxAccountsPerDevice ?? data.max_accounts_per_device), + updatedAtMs: numberValue(data.updatedAtMs ?? data.updated_at_ms), + updatedByAdminId: numberValue(data.updatedByAdminId ?? data.updated_by_admin_id), + }; +} + +function numberValue(value: unknown) { + const number = Number(value ?? 0); + return Number.isFinite(number) ? number : 0; +} + +function stringValue(value: unknown) { + return typeof value === "string" ? value : value == null ? "" : String(value); +} diff --git a/src/features/risk-config/hooks/useRiskConfigPage.js b/src/features/risk-config/hooks/useRiskConfigPage.js new file mode 100644 index 0000000..131b6d0 --- /dev/null +++ b/src/features/risk-config/hooks/useRiskConfigPage.js @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useState } from "react"; +import { parseForm } from "@/shared/forms/validation"; +import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; +import { useToast } from "@/shared/ui/ToastProvider.jsx"; +import { getRiskConfig, updateRiskConfig } from "@/features/risk-config/api"; +import { useRiskConfigAbilities } from "@/features/risk-config/permissions.js"; +import { riskConfigSchema } from "@/features/risk-config/schema"; + +const emptyData = { + appCode: "", + maxAccountsPerDevice: 1, + updatedAtMs: 0, + updatedByAdminId: 0, +}; + +export function useRiskConfigPage() { + const abilities = useRiskConfigAbilities(); + const { showToast } = useToast(); + const [form, setForm] = useState(emptyData); + const [loadingAction, setLoadingAction] = useState(""); + const queryFn = useCallback(() => getRiskConfig(), []); + const { + data = emptyData, + error, + loading, + reload, + } = useAdminQuery(queryFn, { + errorMessage: "加载风控配置失败", + initialData: emptyData, + queryKey: ["risk-config", "register"], + }); + + useEffect(() => { + if (!data) { + return; + } + setForm({ + appCode: data.appCode || "", + maxAccountsPerDevice: Number(data.maxAccountsPerDevice || 1), + updatedAtMs: Number(data.updatedAtMs || 0), + updatedByAdminId: Number(data.updatedByAdminId || 0), + }); + }, [data]); + + const changeMaxAccountsPerDevice = (value) => { + setForm((current) => ({ ...current, maxAccountsPerDevice: value })); + }; + + const submitConfig = async (event) => { + event.preventDefault(); + setLoadingAction("save"); + try { + const payload = parseForm(riskConfigSchema, { + maxAccountsPerDevice: form.maxAccountsPerDevice, + }); + const saved = await updateRiskConfig({ + max_accounts_per_device: payload.maxAccountsPerDevice, + }); + setForm(saved); + await reload(); + showToast("风控配置已保存", "success"); + } catch (err) { + showToast(err.message || "保存失败", "error"); + } finally { + setLoadingAction(""); + } + }; + + return { + abilities, + changeMaxAccountsPerDevice, + data, + error, + form, + loading, + loadingAction, + reload, + submitConfig, + }; +} diff --git a/src/features/risk-config/pages/RiskConfigPage.jsx b/src/features/risk-config/pages/RiskConfigPage.jsx new file mode 100644 index 0000000..369e121 --- /dev/null +++ b/src/features/risk-config/pages/RiskConfigPage.jsx @@ -0,0 +1,65 @@ +import SaveOutlined from "@mui/icons-material/SaveOutlined"; +import ShieldOutlined from "@mui/icons-material/ShieldOutlined"; +import TextField from "@mui/material/TextField"; +import { AdminActionIconButton, AdminListBody, AdminListPage, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx"; +import { DataState } from "@/shared/ui/DataState.jsx"; +import { TimeText } from "@/shared/ui/TimeText.jsx"; +import { useRiskConfigPage } from "@/features/risk-config/hooks/useRiskConfigPage.js"; +import styles from "@/features/risk-config/risk-config.module.css"; + +export function RiskConfigPage() { + const page = useRiskConfigPage(); + const canSave = page.abilities.canUpdate && page.loadingAction !== "save"; + + return ( + + + + + ) : null + } + /> + + +
+
+
+ + + +
+

注册限制

+ + {page.form.updatedAtMs ? ( + + ) : ( + "默认配置" + )} + +
+
+ + page.changeMaxAccountsPerDevice(event.target.value)} + /> +
+
+
+
+
+ ); +} diff --git a/src/features/risk-config/permissions.js b/src/features/risk-config/permissions.js new file mode 100644 index 0000000..9e8ab84 --- /dev/null +++ b/src/features/risk-config/permissions.js @@ -0,0 +1,11 @@ +import { useAuth } from "@/app/auth/AuthProvider.jsx"; +import { PERMISSIONS } from "@/app/permissions"; + +export function useRiskConfigAbilities() { + const { can } = useAuth(); + + return { + canUpdate: can(PERMISSIONS.riskConfigUpdate), + canView: can(PERMISSIONS.riskConfigView), + }; +} diff --git a/src/features/risk-config/risk-config.module.css b/src/features/risk-config/risk-config.module.css new file mode 100644 index 0000000..ef6ecb1 --- /dev/null +++ b/src/features/risk-config/risk-config.module.css @@ -0,0 +1,53 @@ +.configForm { + max-width: 520px; +} + +.configSection { + display: grid; + gap: var(--space-5); + padding: var(--space-5); + border: 1px solid var(--border); + border-radius: var(--radius-card); + background: var(--bg-card); +} + +.configHeader { + display: flex; + min-width: 0; + align-items: center; + gap: var(--space-3); +} + +.configIcon { + display: inline-flex; + width: 36px; + height: 36px; + flex: 0 0 auto; + align-items: center; + justify-content: center; + border-radius: var(--radius-control); + background: var(--primary-soft); + color: var(--primary); +} + +.configTitle { + min-width: 0; +} + +.configTitle h2 { + margin: 0; + color: var(--text-primary); + font-size: 16px; + font-weight: 700; +} + +.configTitle span { + color: var(--text-tertiary); + font-size: var(--admin-font-size); +} + +@media (max-width: 720px) { + .configForm { + max-width: none; + } +} diff --git a/src/features/risk-config/routes.js b/src/features/risk-config/routes.js new file mode 100644 index 0000000..913258e --- /dev/null +++ b/src/features/risk-config/routes.js @@ -0,0 +1,12 @@ +import { MENU_CODES, PERMISSIONS } from "@/app/permissions"; + +export const riskConfigRoutes = [ + { + label: "风控管理", + loader: () => import("./pages/RiskConfigPage.jsx").then((module) => module.RiskConfigPage), + menuCode: MENU_CODES.appUserRiskConfig, + pageKey: "app-user-risk-config", + path: "/app/users/risk-config", + permission: PERMISSIONS.riskConfigView, + }, +]; diff --git a/src/features/risk-config/schema.ts b/src/features/risk-config/schema.ts new file mode 100644 index 0000000..3ebb66c --- /dev/null +++ b/src/features/risk-config/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const riskConfigSchema = z.object({ + maxAccountsPerDevice: z.coerce.number().int("单设备最大账号数必须是整数").min(1, "单设备最大账号数必须大于 0"), +}); + +export type RiskConfigForm = z.infer; diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index fa92c05..1b5fcc1 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -118,6 +118,7 @@ export const API_OPERATIONS = { getResource: "getResource", getResourceGroup: "getResourceGroup", getResourceIdentityAutoGrantConfig: "getResourceIdentityAutoGrantConfig", + getRiskConfig: "getRiskConfig", getRoleDataScopes: "getRoleDataScopes", getRoomConfig: "getRoomConfig", getRoomRocketConfig: "getRoomRocketConfig", @@ -126,6 +127,7 @@ export const API_OPERATIONS = { getRoomTurnoverRewardConfig: "getRoomTurnoverRewardConfig", getRoomWhitelistConfig: "getRoomWhitelistConfig", getSevenDayCheckInConfig: "getSevenDayCheckInConfig", + getTemporaryPaymentLink: "getTemporaryPaymentLink", getUser: "getUser", getWeeklyStarCycle: "getWeeklyStarCycle", getWheelConfig: "getWheelConfig", @@ -197,6 +199,7 @@ export const API_OPERATIONS = { listSystemMenus: "listSystemMenus", listTaskDefinitions: "listTaskDefinitions", listTeamSalaryPolicies: "listTeamSalaryPolicies", + listTemporaryPaymentLinks: "listTemporaryPaymentLinks", listThirdPartyPaymentChannels: "listThirdPartyPaymentChannels", listUserLeaderboards: "listUserLeaderboards", listUsers: "listUsers", @@ -270,6 +273,7 @@ export const API_OPERATIONS = { updateResourceGroup: "updateResourceGroup", updateResourceGroupItems: "updateResourceGroupItems", updateResourceIdentityAutoGrantConfig: "updateResourceIdentityAutoGrantConfig", + updateRiskConfig: "updateRiskConfig", updateRole: "updateRole", updateRoom: "updateRoom", updateRoomConfig: "updateRoomConfig", @@ -1031,6 +1035,13 @@ export const API_ENDPOINTS: Record = { permission: "resource-group:view", permissions: ["resource-group:view"] }, + getRiskConfig: { + method: "GET", + operationId: API_OPERATIONS.getRiskConfig, + path: "/v1/admin/users/risk-config", + permission: "risk-config:view", + permissions: ["risk-config:view"] + }, getRoleDataScopes: { method: "GET", operationId: API_OPERATIONS.getRoleDataScopes, @@ -1087,6 +1098,13 @@ export const API_ENDPOINTS: Record = { permission: "seven-day-checkin:view", permissions: ["seven-day-checkin:view"] }, + getTemporaryPaymentLink: { + method: "GET", + operationId: API_OPERATIONS.getTemporaryPaymentLink, + path: "/v1/admin/payment/temporary-links/{order_id}", + permission: "payment-temporary-link:view", + permissions: ["payment-temporary-link:view"] + }, getUser: { method: "GET", operationId: API_OPERATIONS.getUser, @@ -1582,6 +1600,13 @@ export const API_ENDPOINTS: Record = { permission: "team-salary-policy:view", permissions: ["team-salary-policy:view"] }, + listTemporaryPaymentLinks: { + method: "GET", + operationId: API_OPERATIONS.listTemporaryPaymentLinks, + path: "/v1/admin/payment/temporary-links", + permission: "payment-temporary-link:view", + permissions: ["payment-temporary-link:view"] + }, listThirdPartyPaymentChannels: { method: "GET", operationId: API_OPERATIONS.listThirdPartyPaymentChannels, @@ -2081,6 +2106,13 @@ export const API_ENDPOINTS: Record = { permission: "resource-group:update", permissions: ["resource-group:update"] }, + updateRiskConfig: { + method: "PUT", + operationId: API_OPERATIONS.updateRiskConfig, + path: "/v1/admin/users/risk-config", + permission: "risk-config:update", + permissions: ["risk-config:update"] + }, updateRole: { method: "PATCH", operationId: API_OPERATIONS.updateRole, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 38a6a99..8f835e8 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -1828,6 +1828,38 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/payment/temporary-links": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listTemporaryPaymentLinks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/payment/temporary-links/{order_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getTemporaryPaymentLink"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/payment/third-party-channels": { parameters: { query?: never; @@ -2532,6 +2564,22 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/users/risk-config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getRiskConfig"]; + put: operations["updateRiskConfig"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/app/users": { parameters: { query?: never; @@ -5925,6 +5973,32 @@ export interface operations { 200: components["responses"]["EmptyResponse"]; }; }; + listTemporaryPaymentLinks: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; + getTemporaryPaymentLink: { + parameters: { + query?: never; + header?: never; + path: { + order_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; listThirdPartyPaymentChannels: { parameters: { query?: never; @@ -6704,6 +6778,36 @@ export interface operations { 200: components["responses"]["EmptyResponse"]; }; }; + getRiskConfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; + updateRiskConfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + max_accounts_per_device: number; + }; + }; + }; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; appListUsers: { parameters: { query?: never; diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index fac3286..d66e08f 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -473,6 +473,30 @@ export interface ThirdPartyPaymentChannelDto { status?: string; } +export interface TemporaryPaymentLinkDto { + appCode?: string; + audienceType?: string; + coinAmount: number; + countryCode?: string; + createdAtMs?: number; + currencyCode?: string; + failureReason?: string; + orderId: string; + payType?: string; + payUrl?: string; + payWay?: string; + paymentMethodId?: EntityId; + productName?: string; + providerAmountMinor?: number; + providerCode?: string; + providerOrderId?: string; + status?: string; + transactionId?: string; + txHash?: string; + updatedAtMs?: number; + usdMinorAmount: number; +} + export interface ThirdPartyPaymentRateSyncDto { missingCurrencies?: string[]; rates?: Record; @@ -730,6 +754,7 @@ export interface CoinSellerSalaryRatesPayload { export interface AppUserDto { avatar?: string; + balances?: AppUserAssetBalanceDto[]; coin?: number; country?: string; countryDisplayName?: string; @@ -738,16 +763,57 @@ export interface AppUserDto { defaultDisplayUserId?: string; diamond?: number; displayUserId?: string; + equippedResources?: AppUserResourceDto[]; gender?: string; lastActiveAtMs?: number; prettyDisplayUserId?: string; prettyId?: string; regionId?: number; regionName?: string; + resources?: AppUserResourceDto[]; status?: string; updatedAtMs?: number; userId: string; username?: string; + vip?: AppUserVIPDto; +} + +export interface AppUserAssetBalanceDto { + assetType: string; + availableAmount: number; + frozenAmount: number; + totalAmount: number; + version: number; + updatedAtMs: number; +} + +export interface AppUserVIPDto { + active: boolean; + expiresAtMs: number; + level: number; + name?: string; + startedAtMs: number; + updatedAtMs: number; +} + +export interface AppUserResourceDto { + animationUrl?: string; + assetUrl?: string; + createdAtMs: number; + effectiveAtMs: number; + entitlementId: string; + equipped: boolean; + expiresAtMs: number; + name: string; + previewUrl?: string; + quantity: number; + remainingQuantity: number; + resourceCode: string; + resourceId: number; + resourceType: string; + sourceGrantId?: string; + status: string; + updatedAtMs: number; } export interface AppUserBriefDto {