From f11678684692a02abbd5f357fa37bec8190db051 Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 15 Sep 2026 18:44:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(base):=20GetUserMod=20=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=20UserBase=20=E4=B8=8B=E5=8F=91=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=A0=91=EF=BC=8C=E7=A7=BB=E9=99=A4=E7=A1=AC?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与其他子应用(Notice / svc-hall / 事项库)统一:GetUserMod 不再返回本应用硬编码 MENUS, 改经平台 SDK YschBaseOpenApi.getUserModule(userId, schId) 取当前用户在本应用 (yh.open.app-key=100019)下的已授权模块树,层级由平台契约 BoaModule.children 承载, 本侧仅做前端 layout 契约的字段改名映射(toFrontendTree),不再本地组树。 设计概览 §9.4「菜单为本地路由约定、不进 app_module」的口径自此作废。 两处刻意的差异与取舍: 1) modId 取 custom_id 而非主键 id:front router/routes.ts 的 meta.modId 约定是 100~105, 等于 app_module.custom_id,旧硬编码菜单发的也是这些值;若照抄其他子应用「modId=主键id」, 前端 modId 兜底匹配会永久失配(仅剩 path 匹配可用)。故 custom_id 可解析为数字时取之, 否则回退主键 id。 2) 不保留本地 MENUS 兜底:静默回退会把「UserBase 未配 app_module / 未授权」伪装成正常。 配置缺失应表现为空菜单并被立刻发现。发布顺序必须先是 flow-center接入UserBase菜单.sql (含 app_module 写入与角色/组授权),再发本应用。 新增 FlowBaseControllerTest 4 例:层级原样保留与字段改名、children 恒为数组(含上游未展开 的 null 归零)、custom_id 非数字回退主键、path 缺失给空串且 null/空入参不抛 NPE。 映射方法放宽为包级可见以免为测试改写 YschBaseOpenApi 全局静态客户端、污染共享 JVM 内其它用例。 --- .../controller/api/FlowBaseController.java | 93 ++++++++++++++----- .../api/FlowBaseControllerTest.java | 89 ++++++++++++++++++ 2 files changed, 157 insertions(+), 25 deletions(-) create mode 100644 src/test/java/com/yunhai/flowcenter/controller/api/FlowBaseControllerTest.java diff --git a/src/main/java/com/yunhai/flowcenter/controller/api/FlowBaseController.java b/src/main/java/com/yunhai/flowcenter/controller/api/FlowBaseController.java index 3c9d473..475e3d7 100644 --- a/src/main/java/com/yunhai/flowcenter/controller/api/FlowBaseController.java +++ b/src/main/java/com/yunhai/flowcenter/controller/api/FlowBaseController.java @@ -1,49 +1,92 @@ package com.yunhai.flowcenter.controller.api; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.model.BoaModule; import com.yunhai.platform.utils.model.BoaUser; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** - * 前端壳基础端点(对齐 UserBase BaseController 的 /api/base/* 形态): - * GetUserInfo 返回当前登录人;GetUserMod 返回本地路由菜单——flow-center 是独立登录态应用, - * 菜单不走 UserBase app_module 下发(概览 §9.4),modId 为本应用内部约定。 + * 前端壳基础端点(对齐 UserBase BaseController 的 /api/base/* 形态):GetUserInfo / GetUserMod。 + *

GetUserMod 与其他子应用(Notice / svc-hall / 事项库 Cms)统一走平台下发:经 SDK + * {@code YschBaseOpenApi.getUserModule} 取当前用户在本应用({@code yh.open.app-key=100019})下的 + * 已授权模块树,层级由平台契约 {@code BoaModule.children} 承载,本侧仅映射为前端 layout + * 期望的字段形状({@code page/modId/children})。 + *

设计概览 §9.4「菜单为本地路由约定、不进 app_module」的口径自 2026-09-15 起作废,改为平台统一下发。 */ @RestController public class FlowBaseController { - /** 左侧菜单(本地路由,顺序即展示顺序) */ - private static final String[][] MENUS = { - {"100", "工作台", "/workbench"}, - {"101", "我的办件", "/my"}, - {"102", "办件记录", "/admin/instances"}, - {"103", "流程定义", "/admin/defs"}, - {"104", "办理人映射", "/admin/maps"}, - {"105", "回调管理", "/admin/callbacks"}, - }; - @GetMapping("/api/base/GetUserInfo") public BoaUser getUserInfo(BoaUser authUserWithDept) { return authUserWithDept; } + /** + * 当前用户已授权模块树(左侧菜单)。 + *

依赖 UserBase 侧数据:本应用的 {@code app_module} 行({@code path} 须逐字对上 + * front 路由,见 router/routes.ts)+ 角色/组授权,二者缺一即返回空菜单。 + * 这里不保留本地 MENUS 兜底——静默回退会把「平台未配模块/未授权」伪装成正常, + * 与其他三个子应用口径一致,配置缺失应表现为空菜单并被发现。 + */ @GetMapping("/api/base/GetUserMod") - public List> getUserMod() { - List> modList = new ArrayList<>(); - for (String[] m : MENUS) { - Map item = new HashMap<>(); - item.put("id", Integer.valueOf(m[0])); - item.put("modId", Integer.valueOf(m[0])); - item.put("name", m[1]); - item.put("page", m[2]); - item.put("children", new ArrayList<>()); - modList.add(item); + public List> getUserMod(BoaUser authUserWithDept) { + if (authUserWithDept == null) { + return Collections.emptyList(); } - return modList; + Integer schId = authUserWithDept.getCurrentRole() != null + ? authUserWithDept.getCurrentRole().getSchId() : null; + List modules = YschBaseOpenApi.getUserModule(authUserWithDept.getId(), schId); + if (modules == null || modules.isEmpty()) { + return Collections.emptyList(); + } + // 用户中心已按 BoaModule.children 下发模块树:本地不再重组层级,仅做前端字段改名映射 + return toFrontendTree(modules); + } + + /** + * BoaModule 树 → 前端 layout 期望的字段形状(递归,只改名不组树;Notice/svc-hall/Cms 同款口径)。
+ * 层级与兄弟顺序均由用户中心决定(customId 升序),本侧不重排;叶子 children 为空列表。
+ * 包级可见而非 private:供同包单测直接覆盖映射,避免为测试改写 {@code YschBaseOpenApi} 全局静态客户端。 + */ + static List> toFrontendTree(List modules) { + List> result = new ArrayList<>(); + if (modules == null || modules.isEmpty()) { + return result; + } + for (BoaModule m : modules) { + Map node = new LinkedHashMap<>(); + node.put("id", m.getId()); + node.put("modId", resolveModId(m)); + node.put("name", m.getName()); + node.put("parentId", m.getParentId() != null ? m.getParentId() : 0); + node.put("page", m.getPath() != null ? m.getPath() : ""); + node.put("level", m.getLevel() != null ? m.getLevel() : 0); + node.put("icon", m.getIcon() != null ? m.getIcon() : ""); + node.put("orderNumber", m.getSortNo() != null ? m.getSortNo() : 0); + node.put("usable", true); + node.put("children", toFrontendTree(m.getChildren())); + result.add(node); + } + return result; + } + + /** + * modId 取值:优先 {@code customId}(数值),以对上 front {@code router/routes.ts} 的 + * {@code meta.modId}(100~105,即 app_module.custom_id)约定;customId 非数字或缺失时回退主键 id。 + * 注意:不能像其他子应用那样直接取主键 id——那会让前端 modId 兜底匹配永久失配(path 匹配仍可用)。 + */ + private static Object resolveModId(BoaModule m) { + String customId = m.getCustomId(); + if (customId != null && customId.trim().matches("\\d+")) { + return Integer.valueOf(customId.trim()); + } + return m.getId(); } } diff --git a/src/test/java/com/yunhai/flowcenter/controller/api/FlowBaseControllerTest.java b/src/test/java/com/yunhai/flowcenter/controller/api/FlowBaseControllerTest.java new file mode 100644 index 0000000..5c6bcc6 --- /dev/null +++ b/src/test/java/com/yunhai/flowcenter/controller/api/FlowBaseControllerTest.java @@ -0,0 +1,89 @@ +package com.yunhai.flowcenter.controller.api; + +import com.yunhai.platform.utils.model.BoaModule; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * GetUserMod 映射单测:flow-center 已改为直接消费 UserBase 下发的模块树({@code BoaModule.children}), + * 本侧只做前端 layout 契约的字段改名,不再本地组树。
+ * 另锁定两个易错点:modId 必须取 custom_id(对上 routes.ts 的 meta.modId 100~105), + * 以及 children 恒为数组(叶子为空列表、上游未展开的 null 也要归零),否则前端递归会炸。 + */ +class FlowBaseControllerTest { + + @Test + void toFrontendTree_shouldKeepServerHierarchyAndRenameFields() { + BoaModule parent = module(501, "100", "/workbench", 1); + BoaModule child = module(502, "101", "/my", 2); + parent.setChildren(new ArrayList<>(Collections.singletonList(child))); + BoaModule otherRoot = module(503, "105", "/admin/callbacks", 1); + + List> tree = FlowBaseController.toFrontendTree(Arrays.asList(parent, otherRoot)); + + assertEquals(2, tree.size(), "顶层只应有两个根,子节点不得浮到顶层"); + Map first = tree.get(0); + assertEquals("/workbench", first.get("page"), "path → page(前端按 page 匹配菜单高亮)"); + assertEquals(100, first.get("modId"), "modId 必须取 custom_id 而非主键 id(routes.ts meta.modId 约定)"); + assertEquals(501, first.get("id"), "原始主键仍经 id 字段透出"); + + @SuppressWarnings("unchecked") + List> children = (List>) first.get("children"); + assertEquals(1, children.size(), "层级应原样保留"); + assertEquals(101, children.get(0).get("modId")); + assertEquals("/my", children.get(0).get("page")); + } + + @Test + void toFrontendTree_shouldAlwaysEmitChildrenAsArray() { + BoaModule configuredLeaf = module(601, "102", "/admin/instances", 1); + configuredLeaf.setChildren(new ArrayList<>()); + BoaModule notExpanded = module(602, "103", "/admin/defs", 1); // 上游未展开 → children 为 null + + List> tree = FlowBaseController.toFrontendTree(Arrays.asList(configuredLeaf, notExpanded)); + + for (Map node : tree) { + assertNotNull(node.get("children"), "children 不得为 null,前端要无判空递归"); + assertTrue(((List) node.get("children")).isEmpty(), "无子节点时应为空列表"); + } + } + + @Test + void toFrontendTree_shouldFallBackToPrimaryKeyWhenCustomIdNotNumeric() { + BoaModule m = module(701, "workbench-home", "/workbench", 1); + + List> tree = FlowBaseController.toFrontendTree(Collections.singletonList(m)); + + assertEquals(701, tree.get(0).get("modId"), "custom_id 非数字时回退模块主键 id"); + } + + @Test + void toFrontendTree_shouldTolerateNullPathAndEmptyInput() { + BoaModule noPath = module(801, "104", null, 1); + + assertEquals("", FlowBaseController.toFrontendTree(Collections.singletonList(noPath)).get(0).get("page"), + "path 缺失应给空串而非 null(前端字符串比较不判空)"); + assertTrue(FlowBaseController.toFrontendTree(Collections.emptyList()).isEmpty()); + assertTrue(FlowBaseController.toFrontendTree(null).isEmpty(), "上游返回 null 不得抛 NPE"); + } + + private static BoaModule module(Integer id, String customId, String path, Integer level) { + BoaModule m = new BoaModule(); + m.setId(id); + m.setCustomId(customId); + m.setName("模块" + customId); + m.setPath(path); + m.setLevel(level); + m.setSortNo(level * 10); + return m; + } +}