diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json
index 097f534..dc41c8b 100644
--- a/contracts/admin-openapi.json
+++ b/contracts/admin-openapi.json
@@ -8875,8 +8875,11 @@
},
"ApiPageManager": {
"type": "object",
- "required": ["items", "page", "pageSize", "total"],
+ "required": ["items", "page", "pageSize", "total", "bdLeaderInviteScopePolicy"],
"properties": {
+ "bdLeaderInviteScopePolicy": {
+ "$ref": "#/components/schemas/ManagerRoleScopePolicy"
+ },
"items": {
"type": "array",
"items": {
@@ -10112,6 +10115,19 @@
}
}
},
+ "ManagerRoleScopePolicy": {
+ "type": "object",
+ "required": ["baseScope", "regionExpansionConfigurable"],
+ "properties": {
+ "baseScope": {
+ "type": "string",
+ "enum": ["country", "region", "global"]
+ },
+ "regionExpansionConfigurable": {
+ "type": "boolean"
+ }
+ }
+ },
"Manager": {
"type": "object",
"required": ["userId"],
@@ -10128,6 +10144,20 @@
"canAddBdLeader": {
"type": "boolean"
},
+ "canAddBdLeaderInRegion": {
+ "type": "boolean"
+ },
+ "bdLeaderInviteBaseScope": {
+ "type": "string",
+ "enum": ["country", "region", "global"]
+ },
+ "bdLeaderInviteEffectiveScope": {
+ "type": "string",
+ "enum": ["country", "region", "global"]
+ },
+ "bdLeaderRegionExpansionConfigurable": {
+ "type": "boolean"
+ },
"canAddSuperadmin": {
"type": "boolean"
},
@@ -10638,6 +10668,9 @@
"canAddBdLeader": {
"type": "boolean"
},
+ "canAddBdLeaderInRegion": {
+ "type": "boolean"
+ },
"canAddSuperadmin": {
"type": "boolean"
},
@@ -10680,6 +10713,9 @@
"canAddBdLeader": {
"type": "boolean"
},
+ "canAddBdLeaderInRegion": {
+ "type": "boolean"
+ },
"canAddSuperadmin": {
"type": "boolean"
},
diff --git a/src/features/host-org/api.test.ts b/src/features/host-org/api.test.ts
index dd49640..766b13b 100644
--- a/src/features/host-org/api.test.ts
+++ b/src/features/host-org/api.test.ts
@@ -69,6 +69,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
status: "active",
});
await createManager({
+ canAddBdLeaderInRegion: false,
canBlockUser: false,
canGrantBadge: false,
canGrantVip: true,
@@ -76,6 +77,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
targetUserId: "1003",
});
await updateManager("1003", {
+ canAddBdLeaderInRegion: true,
canBlockUser: false,
canGrantVehicle: true,
canGrantVip: false,
@@ -158,6 +160,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
expect(String(managerCreateUrl)).toContain("/api/v1/admin/managers");
expect(managerCreateInit?.method).toBe("POST");
expect(JSON.parse(String(managerCreateInit?.body))).toEqual({
+ canAddBdLeaderInRegion: false,
canBlockUser: false,
canGrantBadge: false,
canGrantVip: true,
@@ -167,6 +170,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
expect(String(managerUpdateUrl)).toContain("/api/v1/admin/managers/1003");
expect(managerUpdateInit?.method).toBe("PUT");
expect(JSON.parse(String(managerUpdateInit?.body))).toEqual({
+ canAddBdLeaderInRegion: true,
canBlockUser: false,
canGrantVehicle: true,
canGrantVip: false,
diff --git a/src/features/host-org/hooks/useHostManagersPage.js b/src/features/host-org/hooks/useHostManagersPage.js
index f536c94..29ed8e7 100644
--- a/src/features/host-org/hooks/useHostManagersPage.js
+++ b/src/features/host-org/hooks/useHostManagersPage.js
@@ -17,29 +17,44 @@ export const managerPermissionFields = [
"canGrantVip",
"canUpdateUserLevel",
"canAddBdLeader",
+ "canAddBdLeaderInRegion",
"canAddAdmin",
"canAddSuperadmin",
"canBlockUser",
"canTransferUserCountry",
];
-const emptyPermissions = () => Object.fromEntries(managerPermissionFields.map((field) => [field, true]));
+const managerPermissionDefault = (field) => field !== "canAddBdLeaderInRegion";
-const emptyManagerForm = () => ({ contact: "", targetUserId: "", ...emptyPermissions() });
+const managerPermissionEnabled = (source, field) => {
+ const value = source?.[field];
+ return value === undefined || value === null ? managerPermissionDefault(field) : value !== false;
+};
+
+const emptyPermissions = () =>
+ Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionDefault(field)]));
+
+const emptyManagerForm = (regionExpansionConfigurable = false) => ({
+ bdLeaderRegionExpansionConfigurable: regionExpansionConfigurable,
+ contact: "",
+ targetUserId: "",
+ ...emptyPermissions(),
+});
const managerPermissionPayload = (form) =>
- Object.fromEntries(managerPermissionFields.map((field) => [field, form[field] !== false]));
+ Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(form, field)]));
const managerStatusPayload = (item, status) => ({
contact: item?.contact || "",
status,
- ...Object.fromEntries(managerPermissionFields.map((field) => [field, item?.[field] !== false])),
+ ...Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(item, field)])),
});
const managerFormFromItem = (item) => ({
+ bdLeaderRegionExpansionConfigurable: item?.bdLeaderRegionExpansionConfigurable === true,
contact: item?.contact || "",
targetUserId: String(item?.userId || ""),
- ...Object.fromEntries(managerPermissionFields.map((field) => [field, item?.[field] !== false])),
+ ...Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(item, field)])),
});
export function useHostManagersPage() {
@@ -118,7 +133,7 @@ export function useHostManagersPage() {
});
showToast("经理已添加", "success");
}
- setManagerForm(emptyManagerForm());
+ setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true));
setActiveAction("");
setEditingManager(null);
await reload();
@@ -132,12 +147,12 @@ export function useHostManagersPage() {
const closeAction = () => {
setActiveAction("");
setEditingManager(null);
- setManagerForm(emptyManagerForm());
+ setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true));
};
const openManagerForm = () => {
setEditingManager(null);
- setManagerForm(emptyManagerForm());
+ setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true));
setActiveAction("manager");
};
diff --git a/src/features/host-org/pages/HostManagersPage.jsx b/src/features/host-org/pages/HostManagersPage.jsx
index c2005d0..d7a6b29 100644
--- a/src/features/host-org/pages/HostManagersPage.jsx
+++ b/src/features/host-org/pages/HostManagersPage.jsx
@@ -22,6 +22,7 @@ const managerPermissionOptions = [
{ key: "canGrantVip", label: "VIP" },
{ key: "canUpdateUserLevel", label: "升级等级" },
{ key: "canAddBdLeader", label: "BD Leader" },
+ { key: "canAddBdLeaderInRegion", label: "区域邀请 BD Leader", regionExpansionOnly: true },
{ key: "canAddAdmin", label: "Admin" },
{ key: "canAddSuperadmin", label: "Superadmin" },
{ key: "canBlockUser", label: "Block User" },
@@ -201,20 +202,28 @@ function ManagerStatusSwitch({ item, page }) {
function ManagerPermissions({ item }) {
return (
- {managerPermissionOptions.map((permission) => {
- const enabled = item?.[permission.key] !== false;
- return (
-
- {permission.label}
- {enabled ? "开" : "关"}
-
- );
- })}
+ {managerPermissionOptions
+ .filter(
+ (permission) =>
+ !permission.regionExpansionOnly || item?.bdLeaderRegionExpansionConfigurable === true,
+ )
+ .map((permission) => {
+ const enabled =
+ permission.key === "canAddBdLeaderInRegion"
+ ? item?.[permission.key] === true
+ : item?.[permission.key] !== false;
+ return (
+
+ {permission.label}
+ {enabled ? "开" : "关"}
+
+ );
+ })}
);
}
@@ -237,19 +246,28 @@ function ManagerActions({ item, page }) {
function PermissionSwitchGrid({ disabled, form, onChange }) {
return (
- {managerPermissionOptions.map((permission) => (
-
- ))}
+ {managerPermissionOptions
+ .filter(
+ (permission) =>
+ !permission.regionExpansionOnly || form?.bdLeaderRegionExpansionConfigurable === true,
+ )
+ .map((permission) => (
+
+ ))}
);
}
diff --git a/src/features/host-org/pages/HostManagersPage.test.jsx b/src/features/host-org/pages/HostManagersPage.test.jsx
index 52aad46..a5cd6fc 100644
--- a/src/features/host-org/pages/HostManagersPage.test.jsx
+++ b/src/features/host-org/pages/HostManagersPage.test.jsx
@@ -17,7 +17,9 @@ afterEach(() => {
test("manager status column uses a switch and toggles through the page hook", () => {
const manager = managerFixture();
- vi.mocked(useHostManagersPage).mockReturnValue(pageFixture({ data: { items: [manager], page: 1, pageSize: 50, total: 1 } }));
+ vi.mocked(useHostManagersPage).mockReturnValue(
+ pageFixture({ data: { items: [manager], page: 1, pageSize: 50, total: 1 } }),
+ );
render();
@@ -31,6 +33,35 @@ test("manager status column uses a switch and toggles through the page hook", ()
expect(mocks.toggleManager).toHaveBeenCalledWith(manager, false);
});
+test("manager form only shows the region invitation switch when backend policy allows configuration", () => {
+ vi.mocked(useHostManagersPage).mockReturnValue(
+ pageFixture({
+ activeAction: "manager",
+ managerForm: {
+ ...pageFixture().managerForm,
+ bdLeaderRegionExpansionConfigurable: true,
+ canAddBdLeaderInRegion: false,
+ },
+ }),
+ );
+
+ const { rerender } = render();
+ expect(screen.getByRole("switch", { name: "区域邀请 BD Leader" })).not.toBeChecked();
+
+ vi.mocked(useHostManagersPage).mockReturnValue(
+ pageFixture({
+ activeAction: "manager",
+ managerForm: {
+ ...pageFixture().managerForm,
+ bdLeaderRegionExpansionConfigurable: false,
+ canAddBdLeaderInRegion: false,
+ },
+ }),
+ );
+ rerender();
+ expect(screen.queryByRole("switch", { name: "区域邀请 BD Leader" })).not.toBeInTheDocument();
+});
+
function pageFixture(patch = {}) {
return {
abilities: {
@@ -50,8 +81,10 @@ function pageFixture(patch = {}) {
loadingAction: "",
loadingRegions: false,
managerForm: {
+ bdLeaderRegionExpansionConfigurable: true,
canAddAdmin: true,
canAddBdLeader: true,
+ canAddBdLeaderInRegion: false,
canAddSuperadmin: true,
canBlockUser: true,
canGrantAvatarFrame: true,
@@ -86,6 +119,8 @@ function managerFixture() {
bdLeaderCount: 3,
canAddAdmin: true,
canAddBdLeader: false,
+ canAddBdLeaderInRegion: false,
+ bdLeaderRegionExpansionConfigurable: true,
canAddSuperadmin: true,
canBlockUser: true,
canGrantAvatarFrame: true,
diff --git a/src/features/host-org/schema.test.ts b/src/features/host-org/schema.test.ts
index 279c900..99c4ec5 100644
--- a/src/features/host-org/schema.test.ts
+++ b/src/features/host-org/schema.test.ts
@@ -132,6 +132,7 @@ test("manager schema defaults vip grant permission on and preserves explicit fal
expect(disabledResult.success).toBe(true);
if (defaultResult.success) {
expect(defaultResult.data.canGrantVip).toBe(true);
+ expect(defaultResult.data.canAddBdLeaderInRegion).toBe(false);
}
if (disabledResult.success) {
expect(disabledResult.data.canGrantVip).toBe(false);
diff --git a/src/features/host-org/schema.ts b/src/features/host-org/schema.ts
index 0b587d0..7e30b36 100644
--- a/src/features/host-org/schema.ts
+++ b/src/features/host-org/schema.ts
@@ -112,6 +112,7 @@ export const createCoinSellerSchema = commandContactSchema.extend({
export const createManagerSchema = z.object({
canAddAdmin: z.boolean().optional().default(true),
canAddBdLeader: z.boolean().optional().default(true),
+ canAddBdLeaderInRegion: z.boolean().optional().default(false),
canAddSuperadmin: z.boolean().optional().default(true),
canBlockUser: z.boolean().optional().default(true),
canGrantAvatarFrame: z.boolean().optional().default(true),
diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts
index 544ac60..d6915d1 100644
--- a/src/shared/api/generated/schema.d.ts
+++ b/src/shared/api/generated/schema.d.ts
@@ -4677,6 +4677,7 @@ export interface components {
total: number;
};
ApiPageManager: {
+ bdLeaderInviteScopePolicy: components["schemas"]["ManagerRoleScopePolicy"];
items: components["schemas"]["Manager"][];
page: number;
pageSize: number;
@@ -5056,11 +5057,22 @@ export interface components {
status?: string;
updatedAtMs?: number;
};
+ ManagerRoleScopePolicy: {
+ /** @enum {string} */
+ baseScope: "country" | "region" | "global";
+ regionExpansionConfigurable: boolean;
+ };
Manager: {
avatar?: string;
bdLeaderCount?: number;
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
+ canAddBdLeaderInRegion?: boolean;
+ /** @enum {string} */
+ bdLeaderInviteBaseScope?: "country" | "region" | "global";
+ /** @enum {string} */
+ bdLeaderInviteEffectiveScope?: "country" | "region" | "global";
+ bdLeaderRegionExpansionConfigurable?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
@@ -5232,6 +5244,7 @@ export interface components {
CreateManagerPayload: {
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
+ canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
@@ -5246,6 +5259,7 @@ export interface components {
UpdateManagerPayload: {
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
+ canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts
index c858f8a..30de140 100644
--- a/src/shared/api/types.ts
+++ b/src/shared/api/types.ts
@@ -1,10 +1,16 @@
export interface ApiPage {
+ bdLeaderInviteScopePolicy?: ManagerRoleScopePolicy;
items: TItem[];
page: number;
pageSize: number;
total: number;
}
+export interface ManagerRoleScopePolicy {
+ baseScope: "country" | "region" | "global";
+ regionExpansionConfigurable: boolean;
+}
+
export interface ApiList {
items: TItem[];
total: number;
@@ -756,6 +762,10 @@ export interface ManagerDto {
bdLeaderCount?: number;
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
+ canAddBdLeaderInRegion?: boolean;
+ bdLeaderInviteBaseScope?: "country" | "region" | "global";
+ bdLeaderInviteEffectiveScope?: "country" | "region" | "global";
+ bdLeaderRegionExpansionConfigurable?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
@@ -779,6 +789,7 @@ export interface ManagerDto {
export interface CreateManagerPayload {
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
+ canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;
@@ -794,6 +805,7 @@ export interface CreateManagerPayload {
export interface UpdateManagerPayload {
canAddAdmin?: boolean;
canAddBdLeader?: boolean;
+ canAddBdLeaderInRegion?: boolean;
canAddSuperadmin?: boolean;
canBlockUser?: boolean;
canGrantAvatarFrame?: boolean;