与其他子应用(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 内其它用例。
This commit is contained in:
@@ -1,49 +1,92 @@
|
|||||||
package com.yunhai.flowcenter.controller.api;
|
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 com.yunhai.platform.utils.model.BoaUser;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 前端壳基础端点(对齐 UserBase BaseController 的 /api/base/* 形态):
|
* 前端壳基础端点(对齐 UserBase BaseController 的 /api/base/* 形态):GetUserInfo / GetUserMod。
|
||||||
* GetUserInfo 返回当前登录人;GetUserMod 返回本地路由菜单——flow-center 是独立登录态应用,
|
* <p>GetUserMod 与其他子应用(Notice / svc-hall / 事项库 Cms)统一走平台下发:经 SDK
|
||||||
* 菜单不走 UserBase app_module 下发(概览 §9.4),modId 为本应用内部约定。
|
* {@code YschBaseOpenApi.getUserModule} 取当前用户在本应用({@code yh.open.app-key=100019})下的
|
||||||
|
* 已授权模块<b>树</b>,层级由平台契约 {@code BoaModule.children} 承载,本侧仅映射为前端 layout
|
||||||
|
* 期望的字段形状({@code page/modId/children})。
|
||||||
|
* <p>设计概览 §9.4「菜单为本地路由约定、不进 app_module」的口径自 2026-09-15 起作废,改为平台统一下发。
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
public class FlowBaseController {
|
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")
|
@GetMapping("/api/base/GetUserInfo")
|
||||||
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||||
return authUserWithDept;
|
return authUserWithDept;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户已授权模块树(左侧菜单)。
|
||||||
|
* <p><b>依赖 UserBase 侧数据</b>:本应用的 {@code app_module} 行({@code path} 须逐字对上
|
||||||
|
* front 路由,见 router/routes.ts)+ 角色/组授权,二者缺一即返回空菜单。
|
||||||
|
* 这里<b>不保留本地 MENUS 兜底</b>——静默回退会把「平台未配模块/未授权」伪装成正常,
|
||||||
|
* 与其他三个子应用口径一致,配置缺失应表现为空菜单并被发现。
|
||||||
|
*/
|
||||||
@GetMapping("/api/base/GetUserMod")
|
@GetMapping("/api/base/GetUserMod")
|
||||||
public List<Map<String, Object>> getUserMod() {
|
public List<Map<String, Object>> getUserMod(BoaUser authUserWithDept) {
|
||||||
List<Map<String, Object>> modList = new ArrayList<>();
|
if (authUserWithDept == null) {
|
||||||
for (String[] m : MENUS) {
|
return Collections.emptyList();
|
||||||
Map<String, Object> 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);
|
|
||||||
}
|
}
|
||||||
return modList;
|
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||||
|
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||||
|
List<BoaModule> modules = YschBaseOpenApi.getUserModule(authUserWithDept.getId(), schId);
|
||||||
|
if (modules == null || modules.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 用户中心已按 BoaModule.children 下发模块树:本地不再重组层级,仅做前端字段改名映射
|
||||||
|
return toFrontendTree(modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BoaModule 树 → 前端 layout 期望的字段形状(递归,<b>只改名不组树</b>;Notice/svc-hall/Cms 同款口径)。<br>
|
||||||
|
* 层级与兄弟顺序均由用户中心决定(customId 升序),本侧不重排;叶子 children 为空列表。<br>
|
||||||
|
* 包级可见而非 private:供同包单测直接覆盖映射,避免为测试改写 {@code YschBaseOpenApi} 全局静态客户端。
|
||||||
|
*/
|
||||||
|
static List<Map<String, Object>> toFrontendTree(List<BoaModule> modules) {
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
if (modules == null || modules.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (BoaModule m : modules) {
|
||||||
|
Map<String, Object> 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 契约的字段改名,不再本地组树。<br>
|
||||||
|
* 另锁定两个易错点: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<Map<String, Object>> tree = FlowBaseController.toFrontendTree(Arrays.asList(parent, otherRoot));
|
||||||
|
|
||||||
|
assertEquals(2, tree.size(), "顶层只应有两个根,子节点不得浮到顶层");
|
||||||
|
Map<String, Object> 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<Map<String, Object>> children = (List<Map<String, Object>>) 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<Map<String, Object>> tree = FlowBaseController.toFrontendTree(Arrays.asList(configuredLeaf, notExpanded));
|
||||||
|
|
||||||
|
for (Map<String, Object> 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<Map<String, Object>> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user