feat(admin): 选人教师树(仿 Notice GetSchoolAll)——组树剔除学生组, YschClassOpenApi.getSchTeaList 按 GroupIds 挂组+空组剪枝 (GET /admin/user/teacherTree); 管理端新增 /def/diagram.png 示意图页进度图 <img> 直嵌
build / build-push (push) Successful in 46s

This commit is contained in:
chenxing
2026-09-17 14:53:05 +08:00
parent 5a7d93a48f
commit 434aa9d185
4 changed files with 274 additions and 1 deletions
@@ -18,6 +18,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -45,6 +47,7 @@ public class FlowAdminController {
private final FlowCallbackAdminService callbackAdminService; private final FlowCallbackAdminService callbackAdminService;
private final FlwInstanceRepository instanceRepository; private final FlwInstanceRepository instanceRepository;
private final FlowDiagramService diagramService; 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); return diagramService.build(def, null, mode);
} }
/**
* 流程定义示意图 PNG 流(2026-09-17,示意图页"进度图"视图):&lt;img&gt; 直嵌走同域 CAS 会话,
* 可见性与 /def/diagram 同口径(requireAdmin)。
*/
@GetMapping("/def/diagram.png")
public ResponseEntity<byte[]> 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") @GetMapping("/map/list")
@@ -146,6 +164,15 @@ public class FlowAdminController {
return mapAdminService.userTree(adminGuard.requireAdmin(user, null)); return mapAdminService.userTree(adminGuard.requireAdmin(user, null));
} }
/**
* 选人组件树·教师版(2026-09-17,仿 Notice /api/base/GetSchoolAll):组树骨架(剔除学生组)+
* userclass 教师按 GroupIds 挂组,管理端「选择用户」统一走它。
*/
@GetMapping("/user/teacherTree")
public List<FlowMapAdminService.SelectUserNodeVo> teacherTree(BoaUser user) {
return mapAdminService.teacherTree(adminGuard.requireAdmin(user, null));
}
// ==================== 回调注册/推送日志/死信 ==================== // ==================== 回调注册/推送日志/死信 ====================
@GetMapping("/callback/regList") @GetMapping("/callback/regList")
@@ -18,7 +18,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 办理人映射管理(概览 §4.7/§六):DEPT_KEY → 办理人显式映射,成员检索经 * 办理人映射管理(概览 §4.7/§六):DEPT_KEY → 办理人显式映射,成员检索经
@@ -42,6 +44,9 @@ public class FlowMapAdminService {
@Value("${flow-center.map.dept-group-type:7}") @Value("${flow-center.map.dept-group-type:7}")
private int deptGroupType = 7; private int deptGroupType = 7;
/** 学生组(UserBase GroupTypeEnum.STUDENT_GROUP),选人组树中整棵剔除(口径同 Notice) */
private static final int GROUP_TYPE_STUDENT = 8;
public List<FlwAssigneeMap> listByDept(Long schId, String deptKey) { public List<FlwAssigneeMap> listByDept(Long schId, String deptKey) {
if (deptKey == null || deptKey.trim().isEmpty()) { if (deptKey == null || deptKey.trim().isEmpty()) {
return mapRepository.findEnabledBySchId(schId); return mapRepository.findEnabledBySchId(schId);
@@ -152,6 +157,118 @@ public class FlowMapAdminService {
return vo; return vo;
} }
/**
* 选人组件树·教师版(2026-09-17 仿 Notice GetSchoolAll 口径):组树只做骨架(剔除学生组),
* 成员来自 userclass(YschClassOpenApi.getSchTeaList)的教师列表,按教师所属 GroupIds 挂组。
* 组内 getGroupUsers 不再混入——旧实现会把管理员等非教师账号挂进树(选人数据不对的根因)。
* 空组(无教师)剪枝;教师归属组全被剔除时挂学校根节点。
*/
public List<SelectUserNodeVo> 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<Integer, SelectUserNodeVo> groups = new LinkedHashMap<>();
List<BoaGroup> 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<com.yunhai.platform.openapi.clazz.model.SchTea> 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<Integer> 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.<BoaGroup>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<BoaGroup> 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.<BoaGroup>emptyList() : group.getChildren()) {
flattenGroups(child, out);
}
}
/** 剪掉无教师叶子后代(isParent 组但 children 全空)的组节点;返回该子树是否仍有内容 */
private boolean pruneEmpty(SelectUserNodeVo node) {
List<SelectUserNodeVo> kept = new ArrayList<>();
for (SelectUserNodeVo child : node.getChildren() == null ? new ArrayList<SelectUserNodeVo>() : 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) */ /** 科室预览/导入项(deptKey=UserBase 组 id) */
@@ -460,7 +460,8 @@ class FlowDiagramServiceTest {
// 真管理守卫(纯逻辑无依赖)+ 其余依赖 mock // 真管理守卫(纯逻辑无依赖)+ 其余依赖 mock
controller = new FlowAdminController(new FlowAdminGuard(), defAdminService, controller = new FlowAdminController(new FlowAdminGuard(), defAdminService,
mock(FlowMapAdminService.class), mock(FlowCallbackAdminService.class), 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) { private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {
@@ -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<FlowMapAdminService.SelectUserNodeVo> 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<String> 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
}
}
}