From 434aa9d185b99173cc8820ae292194a02ed7eb3e Mon Sep 17 00:00:00 2001 From: chenxing Date: Thu, 17 Sep 2026 14:53:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E9=80=89=E4=BA=BA=E6=95=99?= =?UTF-8?q?=E5=B8=88=E6=A0=91(=E4=BB=BF=20Notice=20GetSchoolAll)=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=BB=84=E6=A0=91=E5=89=94=E9=99=A4=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E7=BB=84,=20YschClassOpenApi.getSchTeaList=20=E6=8C=89=20Group?= =?UTF-8?q?Ids=20=E6=8C=82=E7=BB=84+=E7=A9=BA=E7=BB=84=E5=89=AA=E6=9E=9D?= =?UTF-8?q?=20(GET=20/admin/user/teacherTree);=20=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=AB=AF=E6=96=B0=E5=A2=9E=20/def/diagram.png=20=E7=A4=BA?= =?UTF-8?q?=E6=84=8F=E5=9B=BE=E9=A1=B5=E8=BF=9B=E5=BA=A6=E5=9B=BE=20?= =?UTF-8?q?=20=E7=9B=B4=E5=B5=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/api/FlowAdminController.java | 27 ++++ .../service/FlowMapAdminService.java | 117 ++++++++++++++++ .../service/FlowDiagramServiceTest.java | 3 +- .../FlowMapAdminServiceTeacherTreeTest.java | 128 ++++++++++++++++++ 4 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/yunhai/flowcenter/service/FlowMapAdminServiceTeacherTreeTest.java diff --git a/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java b/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java index db6fa97..7369300 100644 --- a/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java +++ b/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java @@ -18,6 +18,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -45,6 +47,7 @@ public class FlowAdminController { private final FlowCallbackAdminService callbackAdminService; private final FlwInstanceRepository instanceRepository; private final FlowDiagramService diagramService; + private final com.yunhai.flowcenter.service.DiagramImageService imageService; // ==================== 流程定义 ==================== @@ -93,6 +96,21 @@ public class FlowAdminController { return diagramService.build(def, null, mode); } + /** + * 流程定义示意图 PNG 流(2026-09-17,示意图页"进度图"视图):<img> 直嵌走同域 CAS 会话, + * 可见性与 /def/diagram 同口径(requireAdmin)。 + */ + @GetMapping("/def/diagram.png") + public ResponseEntity defDiagramPng(BoaUser user, @RequestParam("id") Long id, + @RequestParam(value = "mode", required = false) String mode) { + Long schId = adminGuard.requireAdmin(user, null); + FlwDef def = defAdminService.get(schId, id); + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(org.springframework.http.CacheControl.noStore()) + .body(imageService.renderPng(diagramService.build(def, null, mode))); + } + // ==================== 办理人映射 ==================== @GetMapping("/map/list") @@ -146,6 +164,15 @@ public class FlowAdminController { return mapAdminService.userTree(adminGuard.requireAdmin(user, null)); } + /** + * 选人组件树·教师版(2026-09-17,仿 Notice /api/base/GetSchoolAll):组树骨架(剔除学生组)+ + * userclass 教师按 GroupIds 挂组,管理端「选择用户」统一走它。 + */ + @GetMapping("/user/teacherTree") + public List teacherTree(BoaUser user) { + return mapAdminService.teacherTree(adminGuard.requireAdmin(user, null)); + } + // ==================== 回调注册/推送日志/死信 ==================== @GetMapping("/callback/regList") diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowMapAdminService.java b/src/main/java/com/yunhai/flowcenter/service/FlowMapAdminService.java index 316bfdb..8d60bad 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowMapAdminService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowMapAdminService.java @@ -18,7 +18,9 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * 办理人映射管理(概览 §4.7/§六):DEPT_KEY → 办理人显式映射,成员检索经 @@ -42,6 +44,9 @@ public class FlowMapAdminService { @Value("${flow-center.map.dept-group-type:7}") private int deptGroupType = 7; + /** 学生组(UserBase GroupTypeEnum.STUDENT_GROUP),选人组树中整棵剔除(口径同 Notice) */ + private static final int GROUP_TYPE_STUDENT = 8; + public List listByDept(Long schId, String deptKey) { if (deptKey == null || deptKey.trim().isEmpty()) { return mapRepository.findEnabledBySchId(schId); @@ -152,6 +157,118 @@ public class FlowMapAdminService { return vo; } + /** + * 选人组件树·教师版(2026-09-17 仿 Notice GetSchoolAll 口径):组树只做骨架(剔除学生组), + * 成员来自 userclass(YschClassOpenApi.getSchTeaList)的教师列表,按教师所属 GroupIds 挂组。 + * 组内 getGroupUsers 不再混入——旧实现会把管理员等非教师账号挂进树(选人数据不对的根因)。 + * 空组(无教师)剪枝;教师归属组全被剔除时挂学校根节点。 + */ + public List teacherTree(Long schId) { + BoaGroup subtree; + try { + subtree = groupClient.getGroupById(schId.intValue(), true); + } catch (Exception e) { + throw new BizException(ErrorCode.PARAM_ERROR, "UserBase 组树不可达:" + e.getMessage()); + } + if (subtree == null || subtree.getId() == null) { + throw new BizException(ErrorCode.PARAM_ERROR, "学校组不存在(schId=" + schId + ")"); + } + // 扁平化组(剔除学生组整棵),组节点先建壳 isParent=true + Map groups = new LinkedHashMap<>(); + List flat = new ArrayList<>(); + flattenGroups(subtree, flat); + for (BoaGroup g : flat) { + SelectUserNodeVo vo = new SelectUserNodeVo(); + vo.setId(g.getId()); + vo.setName(g.getName() == null ? "" : g.getName()); + vo.setIsParent(true); + groups.put(g.getId(), vo); + } + // 教师挂组;同一 (教师, 组) 去重,归属组全不在树内时兜底挂根 + try { + List teachers = + com.yunhai.platform.openapi.clazz.YschClassOpenApi.getSchTeaList(schId.intValue()); + if (teachers != null) { + for (com.yunhai.platform.openapi.clazz.model.SchTea tea : teachers) { + if (tea == null || tea.getUserTeaSerId() == null) { + continue; + } + SelectUserNodeVo leaf = new SelectUserNodeVo(); + leaf.setId(tea.getUserTeaSerId()); + leaf.setName(tea.getTeaName() == null ? "" : tea.getTeaName()); + List gids = tea.getGroupIds(); + if ((gids == null || gids.isEmpty()) && tea.getGroupId() != null) { + gids = Collections.singletonList(tea.getGroupId()); + } + boolean attached = false; + if (gids != null) { + for (Integer gid : gids) { + SelectUserNodeVo g = groups.get(gid); + if (g != null) { + g.getChildren().add(leaf); + attached = true; + } + } + } + if (!attached) { + SelectUserNodeVo root = groups.get(schId.intValue()); + if (root != null) { + root.getChildren().add(leaf); + } + } + } + } + } catch (Exception e) { + log.warn("[flow-center] 教师列表拉取失败(schId={}):{}", schId, e.getMessage()); + } + // 建父子结构并剪空组(无教师后代的组不进树,根始终保留) + for (BoaGroup g : flat) { + for (BoaGroup child : g.getChildren() == null ? Collections.emptyList() : g.getChildren()) { + SelectUserNodeVo childVo = child == null ? null : groups.get(child.getId()); + if (childVo != null && groups.containsKey(g.getId())) { + groups.get(g.getId()).getChildren().add(childVo); + } + } + } + SelectUserNodeVo root = groups.get(schId.intValue()); + if (root == null) { + return new ArrayList<>(); + } + pruneEmpty(root); + return Collections.singletonList(root); + } + + /** 组子树扁平化;学生组(type=8)整棵剔除(含其后代) */ + private void flattenGroups(BoaGroup group, List out) { + if (group == null || group.getId() == null) { + return; + } + if (group.getType() != null && group.getType() == GROUP_TYPE_STUDENT) { + return; + } + out.add(group); + for (BoaGroup child : group.getChildren() == null ? Collections.emptyList() : group.getChildren()) { + flattenGroups(child, out); + } + } + + /** 剪掉无教师叶子后代(isParent 组但 children 全空)的组节点;返回该子树是否仍有内容 */ + private boolean pruneEmpty(SelectUserNodeVo node) { + List kept = new ArrayList<>(); + for (SelectUserNodeVo child : node.getChildren() == null ? new ArrayList() : node.getChildren()) { + if (child.getIsParent() != null && child.getIsParent()) { + if (pruneEmpty(child)) { + kept.add(child); + } + } else { + kept.add(child); + } + } + node.getChildren().clear(); + node.getChildren().addAll(kept); + return Boolean.TRUE.equals(node.getIsParent()) && !node.getChildren().isEmpty(); + } + // ==================== 一键拉取教师组 ==================== /** 科室预览/导入项(deptKey=UserBase 组 id) */ diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java index cff6009..f5cb0e7 100644 --- a/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java +++ b/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java @@ -460,7 +460,8 @@ class FlowDiagramServiceTest { // 真管理守卫(纯逻辑无依赖)+ 其余依赖 mock controller = new FlowAdminController(new FlowAdminGuard(), defAdminService, mock(FlowMapAdminService.class), mock(FlowCallbackAdminService.class), - mock(FlwInstanceRepository.class), diagramService); + mock(FlwInstanceRepository.class), diagramService, + mock(com.yunhai.flowcenter.service.DiagramImageService.class)); } private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) { diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowMapAdminServiceTeacherTreeTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowMapAdminServiceTeacherTreeTest.java new file mode 100644 index 0000000..ee623f1 --- /dev/null +++ b/src/test/java/com/yunhai/flowcenter/service/FlowMapAdminServiceTeacherTreeTest.java @@ -0,0 +1,128 @@ +package com.yunhai.flowcenter.service; + +import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; +import com.yunhai.platform.openapi.base.OpenApiGroupClient; +import com.yunhai.platform.openapi.base.OpenApiUserClient; +import com.yunhai.platform.openapi.clazz.OpenApiClassClient; +import com.yunhai.platform.openapi.clazz.YschClassOpenApi; +import com.yunhai.platform.openapi.clazz.model.SchTea; +import com.yunhai.platform.utils.model.BoaGroup; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 选人组件教师树(仿 Notice GetSchoolAll 口径):组树骨架剔除学生组、教师按 GroupIds 挂组、 + * 空组剪除、归属组全被剔除时兜底挂根。 + */ +class FlowMapAdminServiceTeacherTreeTest { + + private FlowMapAdminService service; + private final OpenApiGroupClient groupClient = mock(OpenApiGroupClient.class); + private final OpenApiClassClient classClient = mock(OpenApiClassClient.class, RETURNS_DEEP_STUBS); + private static final OpenApiClassClient ORIGINAL_CLIENT = staticClient(); + + private static OpenApiClassClient staticClient() { + try { + java.lang.reflect.Field f = YschClassOpenApi.class.getDeclaredField("client"); + f.setAccessible(true); + return (OpenApiClassClient) f.get(null); + } catch (Exception e) { + return null; + } + } + + @AfterAll + static void restore() { + if (ORIGINAL_CLIENT != null) { + YschClassOpenApi.setClient(ORIGINAL_CLIENT); + } + } + + @BeforeEach + void setUp() { + service = new FlowMapAdminService(mock(FlwAssigneeMapRepository.class), + mock(OpenApiUserClient.class), groupClient); + YschClassOpenApi.setClient(classClient); + } + + private BoaGroup group(int id, String name, Integer type, BoaGroup... children) { + BoaGroup g = new BoaGroup(id, name); + g.setType(type); + if (children.length > 0) { + g.setChildren(new ArrayList<>(Arrays.asList(children))); + } + return g; + } + + private SchTea tea(int userId, String name, Integer... gids) { + SchTea t = new SchTea(); + t.setUserTeaSerId(userId); + t.setTeaName(name); + t.setGroupIds(new ArrayList<>(Arrays.asList(gids))); + return t; + } + + @Test + void teachersAttachByGroup_studentGroupPruned_emptyGroupHighlighted() { + BoaGroup school = group(3, "云海小学", 1, + group(10, "教师组", 7, group(11, "校长室", 7), group(12, "空科室", 7)), + group(20, "学生组", 8)); + when(groupClient.getGroupById(3, true)).thenReturn(school); + when(classClient.getSchTeaList(3)).thenReturn(new ArrayList<>(Arrays.asList( + tea(101, "刘文娟", 11), tea(102, "测试教师10", 11, 10)))); + + List out = service.teacherTree(3L); + + assertEquals(1, out.size()); + FlowMapAdminService.SelectUserNodeVo root = out.get(0); + assertEquals(1, root.getChildren().size()); // 学生组整棵剔除 + FlowMapAdminService.SelectUserNodeVo jiaoshi = root.getChildren().get(0); + assertEquals("教师组", jiaoshi.getName()); + FlowMapAdminService.SelectUserNodeVo xzs = jiaoshi.getChildren().stream() + .filter(c -> "校长室".equals(c.getName())).findFirst() + .orElseThrow(() -> new AssertionError("校长室应未被剪除")); + assertEquals("校长室", xzs.getName()); + List names = new ArrayList<>(); + xzs.getChildren().forEach(c -> names.add(c.getName())); + assertTrue(names.contains("刘文娟"), "教师应按 GroupIds 挂组"); + // 空组(无教师后代)剪除:教师组直属挂教师 102(getGroupUsers 不再混入) + assertEquals(2, jiaoshi.getChildren().size()); + assertTrue(jiaoshi.getChildren().stream() + .anyMatch(c -> c.getChildren().stream().anyMatch(l -> "测试教师10".equals(l.getName())))); + } + + @Test + void teacherWithoutInTreeGroupFallsBackToRoot() { + BoaGroup school = group(3, "云海小学", 5); + when(groupClient.getGroupById(3, true)).thenReturn(school); + when(classClient.getSchTeaList(3)).thenReturn(new ArrayList<>(Arrays.asList(tea(9, "张三", 999)))); + + FlowMapAdminService.SelectUserNodeVo root = service.teacherTree(3L).get(0); + assertEquals(1, root.getChildren().size()); + assertEquals("张三", root.getChildren().get(0).getName()); + assertTrue(!Boolean.TRUE.equals(root.getChildren().get(0).getIsParent())); + } + + @Test + void schoolGroupMissingThrows() { + when(groupClient.getGroupById(99, true)).thenReturn(null); + try { + service.teacherTree(99L); + org.junit.jupiter.api.Assertions.fail("应当抛业务异常"); + } catch (com.yunhai.platform.apistd.BizException expected) { + // expected + } + } +}