refactor: 菜单栏角色化降噪(role-based-navigation-simplification)
这个提交包含在:
@@ -14,28 +14,81 @@ const BUSINESS_APPROVE_PERMISSIONS = Object.freeze([
|
||||
]);
|
||||
const BUSINESS_APPROVER_ROLE_CODES = new Set(["manager", "ceo", "finance", "finance_mgr", "hr"]);
|
||||
const FINANCE_ROLE_CODES = new Set(["finance", "finance_mgr"]);
|
||||
const TEAM_NAVIGATION_ROLE_CODES = new Set(["manager", "ceo", "finance_mgr", "hr", "admin"]);
|
||||
const FINANCE_PERMISSION_CODES = Object.freeze(["payment.batch.manage", "risk.review.manage", "expense.finance.pay"]);
|
||||
const FINANCE_NAVIGATION_MODULES = new Set(["paymentBatches", "apOperations", "invoicePool"]);
|
||||
const MANAGEMENT_CHILD_MODULES = new Set([
|
||||
"workspace",
|
||||
"people",
|
||||
"organization",
|
||||
"workflowDefinitions",
|
||||
"featureFlags",
|
||||
"auditLogs",
|
||||
"approvalRules",
|
||||
"userManagement",
|
||||
"departments",
|
||||
"masterData",
|
||||
"notificationChannels",
|
||||
"cutover",
|
||||
"erpIntegrations",
|
||||
]);
|
||||
const BACKUP_NAVIGATION_MODULES = new Set([
|
||||
"assets",
|
||||
"weeklyReports",
|
||||
"announcements",
|
||||
"contractsRegistry",
|
||||
"purchaseRequests",
|
||||
"travelRequests",
|
||||
"budgets",
|
||||
"inventoryChecks",
|
||||
"dashboard",
|
||||
]);
|
||||
const NAVIGATION_SECTION_ORDER = Object.freeze({
|
||||
main: [
|
||||
"portal",
|
||||
"workflowTasks",
|
||||
"reimbursements",
|
||||
"paymentBatches",
|
||||
"apOperations",
|
||||
"invoicePool",
|
||||
"projects",
|
||||
"team",
|
||||
"leaveRequests",
|
||||
],
|
||||
admin: ["systemSettings"],
|
||||
backup: [
|
||||
"assets",
|
||||
"weeklyReports",
|
||||
"announcements",
|
||||
"purchaseRequests",
|
||||
"contractsRegistry",
|
||||
"budgets",
|
||||
"attendance",
|
||||
"travelRequests",
|
||||
"inventoryChecks",
|
||||
"dashboard",
|
||||
],
|
||||
});
|
||||
|
||||
const NAVIGATION_REGISTRY = Object.freeze({
|
||||
portal: { key: "portal", label: "工作台", icon: "home_app_logo", href: "/portal", anyOf: [] },
|
||||
team: { key: "team", label: "团队", icon: "groups", href: "/team", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.presenceBoard },
|
||||
leaveRequests: { key: "leaveRequests", label: "请假", icon: "event_available", href: "/leave-requests", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
|
||||
team: { key: "team", label: "团队", icon: "groups", href: "/team", anyOf: ["report.dashboard.view", "project.view_all", "project.manage", "system.users"], featureFlagKey: FEATURE_FLAG_KEYS.presenceBoard },
|
||||
leaveRequests: { key: "leaveRequests", label: "请假记录", icon: "event_available", href: "/leave-requests", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
|
||||
attendance: { key: "attendance", label: "考勤", icon: "schedule", href: "/attendance", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.attendanceUnified },
|
||||
reimbursements: { key: "reimbursements", label: "报销", icon: "request_quote", href: "/reimbursements", anyOf: ["expense.view_own", "expense.view_all"] },
|
||||
assets: { key: "assets", label: "资产", icon: "inventory_2", href: "/assets", anyOf: ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"] },
|
||||
weeklyReports: { key: "weeklyReports", label: "周报", icon: "article", href: "/weekly-reports", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.weeklyReport },
|
||||
todos: { key: "todos", label: "待办", icon: "check_circle", href: "/todos", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.todoBoard },
|
||||
announcements: { key: "announcements", label: "公告", icon: "campaign", href: "/announcements", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.announcements },
|
||||
projects: { key: "projects", label: "项目", icon: "workspaces", href: "/projects", anyOf: ["project.view_own", "project.view_all", "project.manage"], featureFlagKey: FEATURE_FLAG_KEYS.projectDomain },
|
||||
projects: { key: "projects", label: "项目", icon: "workspaces", href: "/projects", anyOf: ["project.view_all", "project.manage"], featureFlagKey: FEATURE_FLAG_KEYS.projectDomain },
|
||||
|
||||
workspace: { key: "workspace", label: "工作区设置", icon: "tune", href: "/system/workspace", anyOf: ["system.settings"] },
|
||||
people: { key: "people", label: "人员管理", icon: "group", href: "/people", anyOf: ["system.users", "report.dashboard.view"], featureFlagKey: FEATURE_FLAG_KEYS.employeeLifecycle },
|
||||
organization: { key: "organization", label: "组织架构", icon: "account_tree", href: "/organization", anyOf: ["system.users", "report.dashboard.view"], featureFlagKey: FEATURE_FLAG_KEYS.employeeLifecycle },
|
||||
contractsRegistry: { key: "contractsRegistry", label: "合同台账", icon: "description", href: "/contracts-registry", anyOf: ["project.manage", "project.view_all", "system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.projectLinking },
|
||||
workflowDefinitions: { key: "workflowDefinitions", label: "流程定义", icon: "schema", href: "/system/workflow-definitions", anyOf: ["system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.workflowDefinitionAdmin },
|
||||
systemSettings: { key: "systemSettings", label: "系统设置", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage"] },
|
||||
featureFlags: { key: "featureFlags", label: "功能开关", icon: "toggle_on", href: "/system/feature-flags", anyOf: ["system.settings"] },
|
||||
systemSettings: { key: "systemSettings", label: "管理", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage", "system.audit.view"] },
|
||||
featureFlags: { key: "featureFlags", label: "功能管理", icon: "toggle_on", href: "/system/feature-flags", anyOf: ["system.settings"] },
|
||||
auditLogs: { key: "auditLogs", label: "审计日志", icon: "manage_search", href: "/audit-logs", anyOf: ["system.audit.view"] },
|
||||
|
||||
approvalRules: { key: "approvalRules", label: "审批规则", icon: "rule", href: "/system/approval-rules", anyOf: ["system.settings"] },
|
||||
@@ -43,17 +96,17 @@ const NAVIGATION_REGISTRY = Object.freeze({
|
||||
departments: { key: "departments", label: "部门管理", icon: "account_tree", href: "/system/departments", anyOf: ["system.users"] },
|
||||
masterData: { key: "masterData", label: "主数据", icon: "dataset", href: "/system/master-data", anyOf: ["masterdata.view", "masterdata.manage"] },
|
||||
notificationChannels: { key: "notificationChannels", label: "通知渠道", icon: "notifications_active", href: "/system/notification-channels", anyOf: ["notification.channel.manage"] },
|
||||
workflowTasks: { key: "workflowTasks", label: "审批待办", icon: "fact_check", href: "/workflow/tasks", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
|
||||
workflowTasks: { key: "workflowTasks", label: "待我处理", icon: "fact_check", href: "/workflow/tasks", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
|
||||
dashboard: { key: "dashboard", label: "仪表盘", icon: "dashboard", href: "/dashboard", anyOf: ["report.dashboard.view"] },
|
||||
purchaseRequests: { key: "purchaseRequests", label: "采购申请", icon: "shopping_cart", href: "/purchase-requests", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] },
|
||||
travelRequests: { key: "travelRequests", label: "差旅申请", icon: "flight", href: "/travel-requests", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
|
||||
budgets: { key: "budgets", label: "预算管理", icon: "account_balance", href: "/budgets", anyOf: ["budget.view", "budget.manage"] },
|
||||
paymentBatches: { key: "paymentBatches", label: "付款批次", icon: "payments", href: "/payment-batches", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
|
||||
apOperations: { key: "apOperations", label: "AP 运营", icon: "account_balance_wallet", href: "/ap-operations", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
|
||||
invoicePool: { key: "invoicePool", label: "票据中心", icon: "receipt_long", href: "/invoice-pool", anyOf: ["invoice.manage"] },
|
||||
paymentBatches: { key: "paymentBatches", label: "付款", icon: "payments", href: "/payment-batches", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
|
||||
apOperations: { key: "apOperations", label: "应付账款", icon: "account_balance_wallet", href: "/ap-operations", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
|
||||
invoicePool: { key: "invoicePool", label: "票据", icon: "receipt_long", href: "/invoice-pool", anyOf: ["invoice.manage"] },
|
||||
inventoryChecks: { key: "inventoryChecks", label: "资产盘点", icon: "inventory", href: "/inventory-checks", anyOf: ["asset.inventory"] },
|
||||
cutover: { key: "cutover", label: "切换控制台", icon: "swap_calls", href: "/system/cutover", anyOf: ["system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.cutoverConsole },
|
||||
erpIntegrations: { key: "erpIntegrations", label: "ERP 集成", icon: "hub", href: "/system/integrations/erp", anyOf: ["integration.manage"] },
|
||||
erpIntegrations: { key: "erpIntegrations", label: "外部系统", icon: "hub", href: "/system/integrations/erp", anyOf: ["integration.manage"] },
|
||||
profile: { key: "profile", label: "个人设置", icon: "manage_accounts", href: "/settings/profile", anyOf: [] },
|
||||
});
|
||||
|
||||
@@ -63,10 +116,10 @@ const DEFAULT_MODULE_VISIBILITY = Object.freeze({
|
||||
leaveRequests: "main",
|
||||
attendance: "hidden",
|
||||
reimbursements: "main",
|
||||
assets: "main",
|
||||
weeklyReports: "main",
|
||||
todos: "main",
|
||||
announcements: "main",
|
||||
assets: "admin",
|
||||
weeklyReports: "admin",
|
||||
todos: "hidden",
|
||||
announcements: "admin",
|
||||
projects: "main",
|
||||
|
||||
workspace: "admin",
|
||||
@@ -198,11 +251,53 @@ function hasBusinessApproverRole(hasPermission, employee) {
|
||||
function hasFinanceRole(hasPermission, employee) {
|
||||
const roleCodes = employee?.roleCodes ?? [];
|
||||
return (
|
||||
roleCodes.some((roleCode) => FINANCE_ROLE_CODES.has(roleCode)) &&
|
||||
(roleCodes.some((roleCode) => FINANCE_ROLE_CODES.has(roleCode)) || roleCodes.includes("admin")) &&
|
||||
hasAnyPermission(hasPermission, employee, FINANCE_PERMISSION_CODES)
|
||||
);
|
||||
}
|
||||
|
||||
function hasTeamNavigationRole(employee) {
|
||||
const roleCodes = employee?.roleCodes ?? [];
|
||||
return roleCodes.some((roleCode) => TEAM_NAVIGATION_ROLE_CODES.has(roleCode));
|
||||
}
|
||||
|
||||
function resolveNavigationSection({ moduleKey, placement, financeNavigation }) {
|
||||
if (placement === "hidden") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (moduleKey === "systemSettings") {
|
||||
return "admin";
|
||||
}
|
||||
|
||||
if (MANAGEMENT_CHILD_MODULES.has(moduleKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (moduleKey === "workflowTasks") {
|
||||
return "main";
|
||||
}
|
||||
|
||||
if (FINANCE_NAVIGATION_MODULES.has(moduleKey)) {
|
||||
return financeNavigation ? "main" : null;
|
||||
}
|
||||
|
||||
if (BACKUP_NAVIGATION_MODULES.has(moduleKey)) {
|
||||
return "backup";
|
||||
}
|
||||
|
||||
return placement === "main" ? "main" : "backup";
|
||||
}
|
||||
|
||||
function orderNavigationItems(items, order) {
|
||||
const rank = new Map(order.map((moduleKey, index) => [moduleKey, index]));
|
||||
return [...items].sort((left, right) => {
|
||||
const leftRank = rank.get(left.key) ?? Number.MAX_SAFE_INTEGER;
|
||||
const rightRank = rank.get(right.key) ?? Number.MAX_SAFE_INTEGER;
|
||||
return leftRank - rightRank;
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveModuleGate({ prisma, config, employee, moduleKey, registryItem }) {
|
||||
if (!registryItem?.featureFlagKey) {
|
||||
return { enabled: true, reason: "no_feature_gate" };
|
||||
@@ -235,6 +330,10 @@ async function getSystemNavigation({ prisma, config, employee, hasPermission })
|
||||
continue;
|
||||
}
|
||||
|
||||
if (moduleKey === "team" && !hasTeamNavigationRole(employee)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (FINANCE_NAVIGATION_MODULES.has(moduleKey) && !hasFinanceRole(hasPermission, employee)) {
|
||||
continue;
|
||||
}
|
||||
@@ -254,25 +353,30 @@ async function getSystemNavigation({ prisma, config, employee, hasPermission })
|
||||
featureFlagKey: registryItem.featureFlagKey ?? null,
|
||||
};
|
||||
|
||||
if (["purchaseRequests", "travelRequests", "budgets", "paymentBatches", "apOperations", "invoicePool", "inventoryChecks", "cutover", "erpIntegrations", "approvalRules", "userManagement", "departments", "masterData", "notificationChannels", "workflowTasks", "dashboard"].includes(moduleKey)) {
|
||||
sections.backup.push(entry);
|
||||
const section = resolveNavigationSection({
|
||||
moduleKey,
|
||||
placement,
|
||||
financeNavigation: hasFinanceRole(hasPermission, employee),
|
||||
});
|
||||
|
||||
if (!section) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (placement === "admin") {
|
||||
sections.admin.push(entry);
|
||||
} else {
|
||||
sections.main.push(entry);
|
||||
}
|
||||
sections[section].push(entry);
|
||||
}
|
||||
|
||||
return {
|
||||
workspace,
|
||||
sections,
|
||||
sections: {
|
||||
main: orderNavigationItems(sections.main, NAVIGATION_SECTION_ORDER.main),
|
||||
admin: orderNavigationItems(sections.admin, NAVIGATION_SECTION_ORDER.admin),
|
||||
backup: orderNavigationItems(sections.backup, NAVIGATION_SECTION_ORDER.backup),
|
||||
},
|
||||
recommendedRolePresets: [
|
||||
{ key: "owner", label: "老板", roleCodes: ["ceo"], description: "查看团队状态、审批汇总与关键管理入口。" },
|
||||
{ key: "admin", label: "管理员", roleCodes: ["admin"], description: "管理工作区、人员、流程、开关与审计。" },
|
||||
{ key: "employee", label: "员工", roleCodes: ["employee"], description: "日常使用工作台、请假、报销、周报、待办。" },
|
||||
{ key: "employee", label: "员工", roleCodes: ["employee"], description: "日常使用工作台、请假记录、报销和进度查询。" },
|
||||
{ key: "manager", label: "主管", roleCodes: ["manager"], description: "处理审批、查看团队周报与在岗状态。" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -6,13 +6,31 @@ const {
|
||||
} = require("../../src/services/workspaceSettings");
|
||||
const { hasPermission, resolvePermissionsForRoles } = require("../../src/constants/permissions");
|
||||
|
||||
function buildPrisma() {
|
||||
function buildPrisma(enabledFlagKeys = []) {
|
||||
const enabled = new Set(enabledFlagKeys);
|
||||
return {
|
||||
workspaceSetting: {
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
featureFlag: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
findUnique: vi.fn().mockImplementation(async ({ where }) => {
|
||||
if (!enabled.has(where.flag_key)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: `flag-${where.flag_key}`,
|
||||
flag_key: where.flag_key,
|
||||
override_mode: "force_on",
|
||||
rollout_type: "global",
|
||||
department_ids: [],
|
||||
note: "",
|
||||
version: 1,
|
||||
created_at: null,
|
||||
created_by: null,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -37,7 +55,7 @@ function buildConfig() {
|
||||
}
|
||||
|
||||
describe("workspace small-team navigation defaults", () => {
|
||||
it("keeps only the R66 daily path in default main navigation", () => {
|
||||
it("keeps only the small-team task path in default main navigation", () => {
|
||||
const mainModules = Object.entries(DEFAULT_MODULE_VISIBILITY)
|
||||
.filter(([, placement]) => placement === "main")
|
||||
.map(([key]) => key);
|
||||
@@ -47,12 +65,10 @@ describe("workspace small-team navigation defaults", () => {
|
||||
"team",
|
||||
"leaveRequests",
|
||||
"reimbursements",
|
||||
"assets",
|
||||
"weeklyReports",
|
||||
"todos",
|
||||
"announcements",
|
||||
"projects",
|
||||
]);
|
||||
expect(DEFAULT_MODULE_VISIBILITY.todos).toBe("hidden");
|
||||
expect(DEFAULT_MODULE_VISIBILITY.assets).toBe("admin");
|
||||
expect(DEFAULT_MODULE_VISIBILITY.attendance).toBe("hidden");
|
||||
});
|
||||
|
||||
@@ -77,7 +93,7 @@ describe("workspace small-team navigation defaults", () => {
|
||||
expect(workspace.moduleVisibility.attendance).toBe("hidden");
|
||||
});
|
||||
|
||||
it("keeps P2 and finance-only backup modules out of employee navigation", async () => {
|
||||
it("keeps low-frequency and finance-only modules out of employee primary navigation", async () => {
|
||||
const employee = {
|
||||
id: "employee-1",
|
||||
department_id: "dept-1",
|
||||
@@ -86,12 +102,18 @@ describe("workspace small-team navigation defaults", () => {
|
||||
};
|
||||
|
||||
const navigation = await getSystemNavigation({
|
||||
prisma: buildPrisma(),
|
||||
prisma: buildPrisma(["presence_board_v1", "project_domain_v1"]),
|
||||
config: buildConfig(),
|
||||
employee,
|
||||
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
||||
});
|
||||
|
||||
const mainLabels = navigation.sections.main.map((item) => item.label);
|
||||
expect(mainLabels).toEqual(["工作台", "报销", "请假记录"]);
|
||||
expect(mainLabels).not.toContain("待我处理");
|
||||
expect(mainLabels).not.toContain("票据");
|
||||
expect(mainLabels).not.toContain("个人设置");
|
||||
|
||||
const backupKeys = navigation.sections.backup.map((item) => item.key);
|
||||
expect(backupKeys).toContain("purchaseRequests");
|
||||
expect(backupKeys).not.toContain("travelRequests");
|
||||
@@ -104,7 +126,30 @@ describe("workspace small-team navigation defaults", () => {
|
||||
expect(backupKeys).not.toContain("dashboard");
|
||||
});
|
||||
|
||||
it("does not expose workflow task entry to admin-only navigation", async () => {
|
||||
it("shows待我处理 for business approvers and keeps admin-only users out", async () => {
|
||||
const manager = {
|
||||
id: "manager-1",
|
||||
department_id: "dept-1",
|
||||
roleCodes: ["manager"],
|
||||
permissionCodes: resolvePermissionsForRoles(["manager"]),
|
||||
};
|
||||
|
||||
const managerNavigation = await getSystemNavigation({
|
||||
prisma: buildPrisma(["presence_board_v1", "project_domain_v1"]),
|
||||
config: { ...buildConfig(), projectDomainEnabled: true, presenceBoardEnabled: true },
|
||||
employee: manager,
|
||||
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
||||
});
|
||||
|
||||
expect(managerNavigation.sections.main.map((item) => item.label)).toEqual([
|
||||
"工作台",
|
||||
"待我处理",
|
||||
"报销",
|
||||
"项目",
|
||||
"团队",
|
||||
"请假记录",
|
||||
]);
|
||||
|
||||
const adminOnly = {
|
||||
id: "admin-1",
|
||||
department_id: "dept-1",
|
||||
@@ -113,7 +158,7 @@ describe("workspace small-team navigation defaults", () => {
|
||||
};
|
||||
|
||||
const navigation = await getSystemNavigation({
|
||||
prisma: buildPrisma(),
|
||||
prisma: buildPrisma(["payment_batch_v1"]),
|
||||
config: { ...buildConfig(), workflowDefinitionAdminEnabled: true },
|
||||
employee: adminOnly,
|
||||
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
||||
@@ -125,7 +170,32 @@ describe("workspace small-team navigation defaults", () => {
|
||||
...navigation.sections.backup,
|
||||
].map((item) => item.key);
|
||||
|
||||
expect(allKeys).toContain("auditLogs");
|
||||
expect(allKeys).toContain("systemSettings");
|
||||
expect(allKeys).not.toContain("auditLogs");
|
||||
expect(allKeys).not.toContain("workflowTasks");
|
||||
});
|
||||
|
||||
it("uses work names for finance navigation", async () => {
|
||||
const finance = {
|
||||
id: "finance-1",
|
||||
department_id: "dept-1",
|
||||
roleCodes: ["finance"],
|
||||
permissionCodes: resolvePermissionsForRoles(["finance"]),
|
||||
};
|
||||
|
||||
const navigation = await getSystemNavigation({
|
||||
prisma: buildPrisma(["payment_batch_v1"]),
|
||||
config: { ...buildConfig(), paymentBatchEnabled: true },
|
||||
employee: finance,
|
||||
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
||||
});
|
||||
|
||||
const mainLabels = navigation.sections.main.map((item) => item.label);
|
||||
expect(mainLabels).toContain("待我处理");
|
||||
expect(mainLabels).toContain("付款");
|
||||
expect(mainLabels).toContain("应付账款");
|
||||
expect(mainLabels).toContain("票据");
|
||||
expect(mainLabels).not.toContain("AP 运营");
|
||||
expect(mainLabels).not.toContain("付款批次");
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户