app区域限制

This commit is contained in:
zhx 2026-07-13 15:23:55 +08:00
parent 1b9982e52c
commit 01989ee16e
9 changed files with 343 additions and 83 deletions

View File

@ -1130,6 +1130,11 @@
}
},
"/admin/app-config/banners": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"get": {
"operationId": "listBanners",
"responses": {
@ -1152,6 +1157,11 @@
}
},
"/admin/app-config/banners/{banner_id}": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"put": {
"operationId": "updateBanner",
"responses": {
@ -1194,6 +1204,11 @@
}
},
"/admin/app-config/splash-screens": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"get": {
"operationId": "listSplashScreens",
"responses": {
@ -1216,6 +1231,11 @@
}
},
"/admin/app-config/splash-screens/{splash_id}": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"put": {
"operationId": "updateSplashScreen",
"responses": {
@ -1258,6 +1278,11 @@
}
},
"/admin/app-config/popups": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"get": {
"operationId": "listPopups",
"responses": {
@ -1280,6 +1305,11 @@
}
},
"/admin/app-config/popups/{popup_id}": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"put": {
"operationId": "updatePopup",
"responses": {
@ -1436,6 +1466,11 @@
}
},
"/admin/app-config/explore-tabs": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"get": {
"operationId": "listExploreTabs",
"responses": {
@ -1458,6 +1493,11 @@
}
},
"/admin/app-config/explore-tabs/{tab_id}": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"put": {
"operationId": "updateExploreTab",
"responses": {
@ -1500,6 +1540,11 @@
}
},
"/admin/app-config/versions": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"get": {
"operationId": "listAppVersions",
"responses": {
@ -1522,6 +1567,11 @@
}
},
"/admin/app-config/versions/{version_id}": {
"parameters": [
{
"$ref": "#/components/parameters/AppCodeHeader"
}
],
"put": {
"operationId": "updateAppVersion",
"responses": {

View File

@ -1,12 +1,32 @@
import { afterEach, expect, test, vi } from "vitest";
import { setAccessToken, setSelectedAppCode } from "@/shared/api/request";
import {
createAppVersion,
createBanner,
createExploreTab,
createH5Link,
createPopup,
createSplashScreen,
createSystemMessagePushFanout,
deleteAppVersion,
deleteBanner,
deleteExploreTab,
deleteH5Link,
deletePopup,
deleteSplashScreen,
listAppVersions,
listBanners,
listExploreTabs,
listH5Links,
listPopups,
listSplashScreens,
updateAppVersion,
updateBanner,
updateExploreTab,
updateH5Link,
updateH5Links,
updatePopup,
updateSplashScreen,
} from "./api";
afterEach(() => {
@ -56,6 +76,52 @@ test("H5 config API pins every request to its explicit app scope", async () => {
expect(JSON.parse(String(calls[3][1]?.body))).toEqual(payload);
});
test("every APP config API pins reads and writes to the explicit app scope", async () => {
setSelectedAppCode("lalu");
vi.stubGlobal(
"fetch",
vi.fn(async (_url, init) => {
const method = String(init?.method || "GET");
const data = method === "GET" ? { items: [], total: 0 } : method === "DELETE" ? { deleted: true } : {};
return new Response(JSON.stringify({ code: 0, data }), { status: method === "POST" ? 201 : 200 });
}),
);
const appCode = " Huwaa ";
const banner = { bannerType: "h5", coverUrl: "https://cdn.example.com/banner.png", platform: "android", status: "active" } as const;
const splash = { coverUrl: "https://cdn.example.com/splash.png", platform: "android", splashType: "h5", status: "active" } as const;
const popup = { code: "welcome", jumpType: "h5", jumpUrl: "https://h5.example.com/welcome", name: "Welcome", status: "active" } as const;
const explore = { enabled: true, h5Url: "https://h5.example.com/explore", tab: "Explore" };
const version = { buildNumber: 10, downloadUrl: "https://cdn.example.com/app.apk", forceUpdate: false, platform: "android", version: "1.0.0" } as const;
await listBanners(appCode);
await createBanner(appCode, banner);
await updateBanner(appCode, 1, banner);
await deleteBanner(appCode, 1);
await listSplashScreens(appCode);
await createSplashScreen(appCode, splash);
await updateSplashScreen(appCode, 2, splash);
await deleteSplashScreen(appCode, 2);
await listPopups(appCode);
await createPopup(appCode, popup);
await updatePopup(appCode, 3, popup);
await deletePopup(appCode, 3);
await listExploreTabs(appCode);
await createExploreTab(appCode, explore);
await updateExploreTab(appCode, 4, explore);
await deleteExploreTab(appCode, 4);
await listAppVersions(appCode);
await createAppVersion(appCode, version);
await updateAppVersion(appCode, 5, version);
await deleteAppVersion(appCode, 5);
const calls = vi.mocked(fetch).mock.calls;
expect(calls).toHaveLength(20);
calls.forEach(([, init]) => {
expect(init?.headers).toMatchObject({ "X-App-Code": "huwaa" });
});
});
test("system message push fanout API keeps existing operations path", async () => {
vi.stubGlobal(
"fetch",

View File

@ -100,181 +100,201 @@ export function deleteH5Link(appCode: string, key: EntityId): Promise<{ deleted:
}
function appScopeHeaders(appCode: string) {
// H5 配置是强 App 隔离数据;显式固定请求头可避免用户切换 App 时全局请求上下文变化导致写入错误租户。
// APP 配置全部是强 App 隔离数据;显式固定请求头可避免切换 App 时全局请求上下文变化导致写入错误租户。
return { "X-App-Code": String(appCode || "").trim().toLowerCase() };
}
export function listExploreTabs(query: PageQuery = {}): Promise<ApiList<ExploreTabDto>> {
export function listExploreTabs(appCode: string, query: PageQuery = {}): Promise<ApiList<ExploreTabDto>> {
const endpoint = API_ENDPOINTS.listExploreTabs;
return apiRequest<ApiList<ExploreTabDto>>(apiEndpointPath(API_OPERATIONS.listExploreTabs), {
headers: appScopeHeaders(appCode),
method: endpoint.method,
query,
});
}
export function createExploreTab(payload: ExploreTabPayload): Promise<ExploreTabDto> {
export function createExploreTab(appCode: string, payload: ExploreTabPayload): Promise<ExploreTabDto> {
const endpoint = API_ENDPOINTS.createExploreTab;
return apiRequest<ExploreTabDto, ExploreTabPayload>(apiEndpointPath(API_OPERATIONS.createExploreTab), {
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
});
}
export function updateExploreTab(tabId: EntityId, payload: ExploreTabPayload): Promise<ExploreTabDto> {
export function updateExploreTab(appCode: string, tabId: EntityId, payload: ExploreTabPayload): Promise<ExploreTabDto> {
const endpoint = API_ENDPOINTS.updateExploreTab;
return apiRequest<ExploreTabDto, ExploreTabPayload>(
apiEndpointPath(API_OPERATIONS.updateExploreTab, { tab_id: tabId }),
{
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
},
);
}
export function deleteExploreTab(tabId: EntityId): Promise<{ deleted: boolean }> {
export function deleteExploreTab(appCode: string, tabId: EntityId): Promise<{ deleted: boolean }> {
const endpoint = API_ENDPOINTS.deleteExploreTab;
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteExploreTab, { tab_id: tabId }), {
headers: appScopeHeaders(appCode),
method: endpoint.method,
});
}
export function listBanners(query: PageQuery = {}): Promise<ApiList<AppBannerDto>> {
export function listBanners(appCode: string, query: PageQuery = {}): Promise<ApiList<AppBannerDto>> {
const endpoint = API_ENDPOINTS.listBanners;
return apiRequest<ApiList<AppBannerDto>>(apiEndpointPath(API_OPERATIONS.listBanners), {
headers: appScopeHeaders(appCode),
method: endpoint.method,
query,
});
}
export function createBanner(payload: AppBannerPayload): Promise<AppBannerDto> {
export function createBanner(appCode: string, payload: AppBannerPayload): Promise<AppBannerDto> {
const endpoint = API_ENDPOINTS.createBanner;
return apiRequest<AppBannerDto, AppBannerPayload>(apiEndpointPath(API_OPERATIONS.createBanner), {
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
});
}
export function updateBanner(bannerId: EntityId, payload: AppBannerPayload): Promise<AppBannerDto> {
export function updateBanner(appCode: string, bannerId: EntityId, payload: AppBannerPayload): Promise<AppBannerDto> {
const endpoint = API_ENDPOINTS.updateBanner;
return apiRequest<AppBannerDto, AppBannerPayload>(
apiEndpointPath(API_OPERATIONS.updateBanner, { banner_id: bannerId }),
{
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
},
);
}
export function deleteBanner(bannerId: EntityId): Promise<{ deleted: boolean }> {
export function deleteBanner(appCode: string, bannerId: EntityId): Promise<{ deleted: boolean }> {
const endpoint = API_ENDPOINTS.deleteBanner;
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteBanner, { banner_id: bannerId }), {
headers: appScopeHeaders(appCode),
method: endpoint.method,
});
}
export function listSplashScreens(query: PageQuery = {}): Promise<ApiList<AppSplashScreenDto>> {
export function listSplashScreens(appCode: string, query: PageQuery = {}): Promise<ApiList<AppSplashScreenDto>> {
const endpoint = API_ENDPOINTS.listSplashScreens;
return apiRequest<ApiList<AppSplashScreenDto>>(apiEndpointPath(API_OPERATIONS.listSplashScreens), {
headers: appScopeHeaders(appCode),
method: endpoint.method,
query,
});
}
export function createSplashScreen(payload: AppSplashScreenPayload): Promise<AppSplashScreenDto> {
export function createSplashScreen(appCode: string, payload: AppSplashScreenPayload): Promise<AppSplashScreenDto> {
const endpoint = API_ENDPOINTS.createSplashScreen;
return apiRequest<AppSplashScreenDto, AppSplashScreenPayload>(apiEndpointPath(API_OPERATIONS.createSplashScreen), {
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
});
}
export function updateSplashScreen(splashId: EntityId, payload: AppSplashScreenPayload): Promise<AppSplashScreenDto> {
export function updateSplashScreen(appCode: string, splashId: EntityId, payload: AppSplashScreenPayload): Promise<AppSplashScreenDto> {
const endpoint = API_ENDPOINTS.updateSplashScreen;
return apiRequest<AppSplashScreenDto, AppSplashScreenPayload>(
apiEndpointPath(API_OPERATIONS.updateSplashScreen, { splash_id: splashId }),
{
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
},
);
}
export function deleteSplashScreen(splashId: EntityId): Promise<{ deleted: boolean }> {
export function deleteSplashScreen(appCode: string, splashId: EntityId): Promise<{ deleted: boolean }> {
const endpoint = API_ENDPOINTS.deleteSplashScreen;
return apiRequest<{ deleted: boolean }>(
apiEndpointPath(API_OPERATIONS.deleteSplashScreen, { splash_id: splashId }),
{
headers: appScopeHeaders(appCode),
method: endpoint.method,
},
);
}
export function listPopups(query: PageQuery = {}): Promise<ApiList<AppPopupDto>> {
export function listPopups(appCode: string, query: PageQuery = {}): Promise<ApiList<AppPopupDto>> {
const endpoint = API_ENDPOINTS.listPopups;
return apiRequest<ApiList<AppPopupDto>>(apiEndpointPath(API_OPERATIONS.listPopups), {
headers: appScopeHeaders(appCode),
method: endpoint.method,
query,
});
}
export function createPopup(payload: AppPopupPayload): Promise<AppPopupDto> {
export function createPopup(appCode: string, payload: AppPopupPayload): Promise<AppPopupDto> {
const endpoint = API_ENDPOINTS.createPopup;
return apiRequest<AppPopupDto, AppPopupPayload>(apiEndpointPath(API_OPERATIONS.createPopup), {
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
});
}
export function updatePopup(popupId: EntityId, payload: AppPopupPayload): Promise<AppPopupDto> {
export function updatePopup(appCode: string, popupId: EntityId, payload: AppPopupPayload): Promise<AppPopupDto> {
const endpoint = API_ENDPOINTS.updatePopup;
return apiRequest<AppPopupDto, AppPopupPayload>(
apiEndpointPath(API_OPERATIONS.updatePopup, { popup_id: popupId }),
{
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
},
);
}
export function deletePopup(popupId: EntityId): Promise<{ deleted: boolean }> {
export function deletePopup(appCode: string, popupId: EntityId): Promise<{ deleted: boolean }> {
const endpoint = API_ENDPOINTS.deletePopup;
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deletePopup, { popup_id: popupId }), {
headers: appScopeHeaders(appCode),
method: endpoint.method,
});
}
export function listAppVersions(query: PageQuery = {}): Promise<ApiList<AppVersionDto>> {
export function listAppVersions(appCode: string, query: PageQuery = {}): Promise<ApiList<AppVersionDto>> {
const endpoint = API_ENDPOINTS.listAppVersions;
return apiRequest<ApiList<AppVersionDto>>(apiEndpointPath(API_OPERATIONS.listAppVersions), {
headers: appScopeHeaders(appCode),
method: endpoint.method,
query,
});
}
export function createAppVersion(payload: AppVersionPayload): Promise<AppVersionDto> {
export function createAppVersion(appCode: string, payload: AppVersionPayload): Promise<AppVersionDto> {
const endpoint = API_ENDPOINTS.createAppVersion;
return apiRequest<AppVersionDto, AppVersionPayload>(apiEndpointPath(API_OPERATIONS.createAppVersion), {
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
});
}
export function updateAppVersion(versionId: EntityId, payload: AppVersionPayload): Promise<AppVersionDto> {
export function updateAppVersion(appCode: string, versionId: EntityId, payload: AppVersionPayload): Promise<AppVersionDto> {
const endpoint = API_ENDPOINTS.updateAppVersion;
return apiRequest<AppVersionDto, AppVersionPayload>(
apiEndpointPath(API_OPERATIONS.updateAppVersion, { version_id: versionId }),
{
body: payload,
headers: appScopeHeaders(appCode),
method: endpoint.method,
},
);
}
export function deleteAppVersion(versionId: EntityId): Promise<{ deleted: boolean }> {
export function deleteAppVersion(appCode: string, versionId: EntityId): Promise<{ deleted: boolean }> {
const endpoint = API_ENDPOINTS.deleteAppVersion;
return apiRequest<{ deleted: boolean }>(
apiEndpointPath(API_OPERATIONS.deleteAppVersion, { version_id: versionId }),
{
headers: appScopeHeaders(appCode),
method: endpoint.method,
},
);

View File

@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
import { createAppVersion, deleteAppVersion, listAppVersions, updateAppVersion } from "@/features/app-config/api";
import { useAppConfigAbilities } from "@/features/app-config/permissions.js";
import { appVersionSchema } from "@/features/app-config/schema";
@ -19,6 +20,7 @@ const emptyForm = () => ({
});
export function useAppVersionsPage() {
const { appCode } = useAppScope();
const abilities = useAppConfigAbilities();
const confirm = useConfirm();
const { showToast } = useToast();
@ -30,18 +32,26 @@ export function useAppVersionsPage() {
const [loadingAction, setLoadingAction] = useState("");
const filters = useMemo(() => ({ keyword, platform }), [keyword, platform]);
const queryFn = useCallback(() => listAppVersions(filters), [filters]);
const queryFn = useCallback(() => listAppVersions(appCode, filters), [appCode, filters]);
const {
data = emptyData,
error,
loading,
reload
} = useAdminQuery(queryFn, {
enabled: Boolean(appCode),
errorMessage: "加载版本配置失败",
initialData: emptyData,
queryKey: ["app-config", "versions", filters]
queryKey: ["app-config", "versions", appCode, filters]
});
useEffect(() => {
// 安装包和强更策略必须按 App 隔离;切换 App 后清除旧版本编辑态,避免发布到错误租户。
setActiveAction("");
setEditingItem(null);
setFormState(emptyForm());
}, [appCode]);
const setForm = (patch) => {
setFormState((current) => ({ ...current, ...patch }));
};
@ -76,10 +86,10 @@ export function useAppVersionsPage() {
setLoadingAction(action);
try {
if (editingItem) {
await updateAppVersion(editingItem.id, payload);
await updateAppVersion(appCode, editingItem.id, payload);
showToast("版本配置已更新", "success");
} else {
await createAppVersion(payload);
await createAppVersion(appCode, payload);
showToast("版本配置已新增", "success");
}
closeDialog();
@ -103,7 +113,7 @@ export function useAppVersionsPage() {
}
setLoadingAction(`delete:${item.id}`);
try {
await deleteAppVersion(item.id);
await deleteAppVersion(appCode, item.id);
await reload();
showToast("版本配置已删除", "success");
} catch (err) {

View File

@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
import { createBanner, deleteBanner, listBanners, updateBanner } from "@/features/app-config/api";
import { useAppConfigAbilities } from "@/features/app-config/permissions.js";
import { appBannerSchema } from "@/features/app-config/schema";
@ -28,6 +29,7 @@ const emptyForm = () => ({
});
export function useBannerConfigPage() {
const { appCode } = useAppScope();
const abilities = useAppConfigAbilities();
const confirm = useConfirm();
const { showToast } = useToast();
@ -55,18 +57,26 @@ export function useBannerConfigPage() {
}),
[countryCode, displayScope, keyword, platform, regionId, status],
);
const queryFn = useCallback(() => listBanners(filters), [filters]);
const queryFn = useCallback(() => listBanners(appCode, filters), [appCode, filters]);
const {
data = emptyData,
error,
loading,
reload,
} = useAdminQuery(queryFn, {
enabled: Boolean(appCode),
errorMessage: "加载 BANNER 配置失败",
initialData: emptyData,
queryKey: ["app-config", "banners", filters],
queryKey: ["app-config", "banners", appCode, filters],
});
useEffect(() => {
// 切换 App 后丢弃旧租户表单;已发出的请求仍携带旧 appCode只会回写旧租户缓存键。
setActiveAction("");
setEditingItem(null);
setFormState(emptyForm());
}, [appCode]);
const setForm = (patch) => {
setFormState((current) => ({ ...current, ...patch }));
};
@ -105,10 +115,10 @@ export function useBannerConfigPage() {
setLoadingAction(action);
try {
if (editingItem) {
await updateBanner(editingItem.id, payload);
await updateBanner(appCode, editingItem.id, payload);
showToast("BANNER 已更新", "success");
} else {
await createBanner(payload);
await createBanner(appCode, payload);
showToast("BANNER 已新增", "success");
}
closeDialog();
@ -132,7 +142,7 @@ export function useBannerConfigPage() {
}
setLoadingAction(`delete:${item.id}`);
try {
await deleteBanner(item.id);
await deleteBanner(appCode, item.id);
await reload();
showToast("BANNER 已删除", "success");
} catch (err) {

View File

@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
import { createExploreTab, deleteExploreTab, listExploreTabs, updateExploreTab } from "@/features/app-config/api";
import { useAppConfigAbilities } from "@/features/app-config/permissions.js";
import { exploreTabSchema } from "@/features/app-config/schema";
@ -17,6 +18,7 @@ const emptyForm = () => ({
});
export function useExploreConfigPage() {
const { appCode } = useAppScope();
const abilities = useAppConfigAbilities();
const confirm = useConfirm();
const { showToast } = useToast();
@ -28,18 +30,26 @@ export function useExploreConfigPage() {
const [loadingAction, setLoadingAction] = useState("");
const filters = useMemo(() => ({ enabled, keyword }), [enabled, keyword]);
const queryFn = useCallback(() => listExploreTabs(filters), [filters]);
const queryFn = useCallback(() => listExploreTabs(appCode, filters), [appCode, filters]);
const {
data = emptyData,
error,
loading,
reload,
} = useAdminQuery(queryFn, {
enabled: Boolean(appCode),
errorMessage: "加载 Explore 配置失败",
initialData: emptyData,
queryKey: ["app-config", "explore-tabs", filters],
queryKey: ["app-config", "explore-tabs", appCode, filters],
});
useEffect(() => {
// Explore tab 的文案和 H5 地址属于 App 品牌内容,切换 App 后不能保留旧租户编辑态。
setActiveAction("");
setEditingItem(null);
setFormState(emptyForm());
}, [appCode]);
const setForm = (patch) => {
setFormState((current) => ({ ...current, ...patch }));
};
@ -74,10 +84,10 @@ export function useExploreConfigPage() {
setLoadingAction(action);
try {
if (editingItem) {
await updateExploreTab(editingItem.id, payload);
await updateExploreTab(appCode, editingItem.id, payload);
showToast("Explore 配置已更新", "success");
} else {
await createExploreTab(payload);
await createExploreTab(appCode, payload);
showToast("Explore 配置已新增", "success");
}
closeDialog();
@ -92,7 +102,7 @@ export function useExploreConfigPage() {
const toggleExploreTab = async (item, nextEnabled) => {
setLoadingAction(`toggle:${item.id}`);
try {
await updateExploreTab(item.id, {
await updateExploreTab(appCode, item.id, {
enabled: nextEnabled,
h5Url: item.h5Url || "",
sortOrder: item.sortOrder || 0,
@ -119,7 +129,7 @@ export function useExploreConfigPage() {
}
setLoadingAction(`delete:${item.id}`);
try {
await deleteExploreTab(item.id);
await deleteExploreTab(appCode, item.id);
await reload();
showToast("Explore 配置已删除", "success");
} catch (err) {

View File

@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
import { createPopup, deletePopup, listPopups, updatePopup } from "@/features/app-config/api";
import { useAppConfigAbilities } from "@/features/app-config/permissions.js";
import { appPopupSchema } from "@/features/app-config/schema";
@ -23,6 +24,7 @@ const emptyForm = () => ({
});
export function usePopupConfigPage() {
const { appCode } = useAppScope();
const abilities = useAppConfigAbilities();
const confirm = useConfirm();
const { showToast } = useToast();
@ -40,18 +42,26 @@ export function usePopupConfigPage() {
}),
[keyword, status],
);
const queryFn = useCallback(() => listPopups(filters), [filters]);
const queryFn = useCallback(() => listPopups(appCode, filters), [appCode, filters]);
const {
data = emptyData,
error,
loading,
reload,
} = useAdminQuery(queryFn, {
enabled: Boolean(appCode),
errorMessage: "加载弹窗配置失败",
initialData: emptyData,
queryKey: ["app-config", "popups", filters],
queryKey: ["app-config", "popups", appCode, filters],
});
useEffect(() => {
// 弹窗 code 只在单个 App 内唯一;切换时必须丢弃旧编辑态,不能把旧租户内容复制到新租户。
setActiveAction("");
setEditingItem(null);
setFormState(emptyForm());
}, [appCode]);
const setForm = (patch) => {
setFormState((current) => ({ ...current, ...patch }));
};
@ -86,10 +96,10 @@ export function usePopupConfigPage() {
setLoadingAction(action);
try {
if (editingItem) {
await updatePopup(editingItem.id, payload);
await updatePopup(appCode, editingItem.id, payload);
showToast("弹窗配置已更新", "success");
} else {
await createPopup(payload);
await createPopup(appCode, payload);
showToast("弹窗配置已新增", "success");
}
closeDialog();
@ -113,7 +123,7 @@ export function usePopupConfigPage() {
}
setLoadingAction(`delete:${item.id}`);
try {
await deletePopup(item.id);
await deletePopup(appCode, item.id);
await reload();
showToast("弹窗配置已删除", "success");
} catch (err) {

View File

@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
import {
createSplashScreen,
deleteSplashScreen,
@ -32,6 +33,7 @@ const emptyForm = () => ({
});
export function useSplashScreenConfigPage() {
const { appCode } = useAppScope();
const abilities = useAppConfigAbilities();
const confirm = useConfirm();
const { showToast } = useToast();
@ -57,18 +59,26 @@ export function useSplashScreenConfigPage() {
}),
[countryCode, keyword, platform, regionId, status],
);
const queryFn = useCallback(() => listSplashScreens(filters), [filters]);
const queryFn = useCallback(() => listSplashScreens(appCode, filters), [appCode, filters]);
const {
data = emptyData,
error,
loading,
reload,
} = useAdminQuery(queryFn, {
enabled: Boolean(appCode),
errorMessage: "加载开屏配置失败",
initialData: emptyData,
queryKey: ["app-config", "splash-screens", filters],
queryKey: ["app-config", "splash-screens", appCode, filters],
});
useEffect(() => {
// 开屏配置按 App 独立;切换时关闭旧表单,避免旧图片和投放条件被带到新 App。
setActiveAction("");
setEditingItem(null);
setFormState(emptyForm());
}, [appCode]);
const setForm = (patch) => {
setFormState((current) => ({ ...current, ...patch }));
};
@ -106,10 +116,10 @@ export function useSplashScreenConfigPage() {
setLoadingAction(action);
try {
if (editingItem) {
await updateSplashScreen(editingItem.id, payload);
await updateSplashScreen(appCode, editingItem.id, payload);
showToast("开屏配置已更新", "success");
} else {
await createSplashScreen(payload);
await createSplashScreen(appCode, payload);
showToast("开屏配置已新增", "success");
}
closeDialog();
@ -133,7 +143,7 @@ export function useSplashScreenConfigPage() {
}
setLoadingAction(`delete:${item.id}`);
try {
await deleteSplashScreen(item.id);
await deleteSplashScreen(appCode, item.id);
await reload();
showToast("开屏配置已删除", "success");
} catch (err) {

View File

@ -855,7 +855,9 @@ export interface paths {
"/admin/app-config/banners": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -871,7 +873,9 @@ export interface paths {
"/admin/app-config/banners/{banner_id}": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -887,7 +891,9 @@ export interface paths {
"/admin/app-config/splash-screens": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -903,7 +909,9 @@ export interface paths {
"/admin/app-config/splash-screens/{splash_id}": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -919,7 +927,9 @@ export interface paths {
"/admin/app-config/popups": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -935,7 +945,9 @@ export interface paths {
"/admin/app-config/popups/{popup_id}": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -987,7 +999,9 @@ export interface paths {
"/admin/app-config/explore-tabs": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -1003,7 +1017,9 @@ export interface paths {
"/admin/app-config/explore-tabs/{tab_id}": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -1019,7 +1035,9 @@ export interface paths {
"/admin/app-config/versions": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -1035,7 +1053,9 @@ export interface paths {
"/admin/app-config/versions/{version_id}": {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -5725,7 +5745,11 @@ export interface components {
providerStatus?: string;
providerPayType?: string;
receiveAddress?: string;
orderDate?: string;
/**
* Format: int64
* @description 0 退 createdAtMs
*/
orderDateMs?: number;
status: string;
verifyStatus: string;
grantStatus: string;
@ -5783,6 +5807,11 @@ export interface components {
usdAmount: number;
/** @description 补单时为 true订单正常计入充值但服务端强制金币数为 0 */
isMakeup?: boolean;
/**
* Format: int64
* @description
*/
orderDateMs?: number;
/** @enum {string} */
providerCode: "mifapay" | "v5pay" | "usdt";
externalOrderNo: string;
@ -5860,6 +5889,11 @@ export interface components {
chain?: "TRON" | "BSC";
usdAmount?: number;
usdMinorAmount?: number;
/**
* Format: int64
* @description
*/
orderDateMs?: number;
providerCurrencyCode?: string;
providerAmountMinor?: number;
};
@ -7570,7 +7604,9 @@ export interface operations {
listBanners: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7582,7 +7618,9 @@ export interface operations {
createBanner: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7594,7 +7632,9 @@ export interface operations {
updateBanner: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
banner_id: number;
};
@ -7608,7 +7648,9 @@ export interface operations {
deleteBanner: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
banner_id: number;
};
@ -7622,7 +7664,9 @@ export interface operations {
listSplashScreens: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7634,7 +7678,9 @@ export interface operations {
createSplashScreen: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7646,7 +7692,9 @@ export interface operations {
updateSplashScreen: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
splash_id: number;
};
@ -7660,7 +7708,9 @@ export interface operations {
deleteSplashScreen: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
splash_id: number;
};
@ -7674,7 +7724,9 @@ export interface operations {
listPopups: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7686,7 +7738,9 @@ export interface operations {
createPopup: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7698,7 +7752,9 @@ export interface operations {
updatePopup: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
popup_id: number;
};
@ -7712,7 +7768,9 @@ export interface operations {
deletePopup: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
popup_id: number;
};
@ -7812,7 +7870,9 @@ export interface operations {
listExploreTabs: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7824,7 +7884,9 @@ export interface operations {
createExploreTab: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7836,7 +7898,9 @@ export interface operations {
updateExploreTab: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
tab_id: number;
};
@ -7850,7 +7914,9 @@ export interface operations {
deleteExploreTab: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
tab_id: number;
};
@ -7864,7 +7930,9 @@ export interface operations {
listAppVersions: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7876,7 +7944,9 @@ export interface operations {
createAppVersion: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path?: never;
cookie?: never;
};
@ -7888,7 +7958,9 @@ export interface operations {
updateAppVersion: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
version_id: number;
};
@ -7902,7 +7974,9 @@ export interface operations {
deleteAppVersion: {
parameters: {
query?: never;
header?: never;
header: {
"X-App-Code": components["parameters"]["AppCodeHeader"];
};
path: {
version_id: number;
};