package com.yunhai.flowcenter.service; import com.yunhai.flowcenter.entity.FlwAssigneeMap; import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.exception.ErrorCode; import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.platform.openapi.base.OpenApiGroupClient; import com.yunhai.platform.openapi.base.OpenApiUserClient; import com.yunhai.platform.utils.model.BoaGroup; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 办理人映射管理(概览 §4.7/§六):DEPT_KEY → 办理人显式映射,成员检索经 * UserBase /oapi/user/byId 补全姓名快照(检索按 id 精确查;按名检索二期接 UserBase 模糊端点)。 *

* 2026-09-16 新增「一键拉取教师组」:按 {@code dept-group-type}(默认 7=教师组,与 svc-hall * {@code svchall.item.dept-group-type} 同口径)从 UserBase 学校组子树取科室容器,其子组即科室 * (容器无子组时容器本身即科室),deptKey=UserBase 组 id;成员=组内用户(不含子组)。 * 批量落 flw_assignee_map,替代管理端逐人录入。 */ @Slf4j @Service @RequiredArgsConstructor public class FlowMapAdminService { private final FlwAssigneeMapRepository mapRepository; private final OpenApiUserClient userClient; private final OpenApiGroupClient groupClient; /** 科室"容器组"类型(UserBase GroupTypeEnum,默认 7=教师组;口径承袭 svc-hall) */ @Value("${flow-center.map.dept-group-type:7}") private int deptGroupType = 7; public List listByDept(Long schId, String deptKey) { if (deptKey == null || deptKey.trim().isEmpty()) { return mapRepository.findEnabledBySchId(schId); } return mapRepository.findBySchIdAndDeptKeyOrderByUserIdAsc(schId, deptKey.trim()); } /** 新增/启用映射(uk: sch+dept+user;已存在则刷新姓名/状态) */ @Transactional public FlwAssigneeMap save(Long schId, String deptKey, String deptName, Integer userId, String userName) { if (deptKey == null || deptKey.trim().isEmpty() || userId == null) { throw new BizException(ErrorCode.PARAM_ERROR, "deptKey/userId 必填"); } if (userName == null || userName.trim().isEmpty()) { userName = fetchName(userId); } FlwAssigneeMap map = mapRepository.findBySchIdAndDeptKeyAndUserId(schId, deptKey.trim(), userId); if (map == null) { map = new FlwAssigneeMap(); map.setSchId(schId); map.setDeptKey(deptKey.trim()); map.setUserId(userId); } map.setDeptName(deptName); map.setUserName(userName); map.setStatus(0); return mapRepository.save(map); } /** 停用映射(保留历史,DEPT_KEY 解析只取 status=0) */ @Transactional public void disable(Long schId, Long id) { FlwAssigneeMap map = mapRepository.findById(id) .orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "映射不存在:" + id)); if (!map.getSchId().equals(schId)) { throw new BizException(ErrorCode.FORBIDDEN, "映射不属于该学校"); } map.setStatus(1); mapRepository.save(map); } /** 成员检索:按用户 id 从 UserBase 取姓名快照(管理端录入校验) */ public BoaUser lookupUser(Integer userId) { try { return userClient.getUserById(String.valueOf(userId)); } catch (Exception e) { throw new BizException(ErrorCode.PARAM_ERROR, "UserBase 用户检索失败(服务不可达)"); } } // ==================== 选人组件数据源 ==================== /** 选人组件树节点:组=isParent(true)+children;用户=叶子(isParent 缺省 false) */ @Getter @Setter public static class SelectUserNodeVo { private Integer id; private String name; private Boolean isParent; private List children = new ArrayList<>(); } /** * 学校组树(组→组内用户叶子),供管理端「选择用户」弹窗(多选指定用户/单选映射办理人)。 * 与 {@link #previewGroupDepts(Long)} 同源:UserBase getGroupById(schId, withSub) 子树 + 逐组挂成员。 */ public List userTree(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 + ")"); } return Collections.singletonList(groupNode(subtree)); } /** 组节点:先挂用户叶子再递归子组;isParent=UserBase 已宣告子组,防"空组"被当作用户叶子 */ private SelectUserNodeVo groupNode(BoaGroup group) { SelectUserNodeVo vo = new SelectUserNodeVo(); vo.setId(group.getId()); vo.setName(group.getName()); boolean declaredSub = group.getChildren() != null && !group.getChildren().isEmpty(); try { List members = groupClient.getGroupUsers(group.getId(), false, null); if (members != null) { for (BoaUser u : members) { if (u != null && u.getId() != null) { SelectUserNodeVo leaf = new SelectUserNodeVo(); leaf.setId(u.getId()); leaf.setName(u.getName() == null ? "" : u.getName()); vo.getChildren().add(leaf); } } } } catch (Exception e) { log.warn("[flow-center] 组成员拉取失败(groupId={}, name={}):{}", group.getId(), group.getName(), e.getMessage()); } for (BoaGroup child : group.getChildren() == null ? Collections.emptyList() : group.getChildren()) { if (child != null && child.getId() != null) { vo.getChildren().add(groupNode(child)); } } vo.setIsParent(declaredSub || !vo.getChildren().isEmpty()); return vo; } // ==================== 一键拉取教师组 ==================== /** 科室预览/导入项(deptKey=UserBase 组 id) */ @Getter @Setter public static class GroupDeptVo { private String deptKey; private String deptName; private List users = new ArrayList<>(); } @Getter @Setter public static class MemberVo { private Integer userId; private String userName; public MemberVo() { } public MemberVo(Integer userId, String userName) { this.userId = userId; this.userName = userName; } } /** 导入结果计数 */ @Getter @Setter public static class GroupImportResultVo { private int depts; private int users; private int skippedExisting; } /** * 预览:学校子树内教师组容器 → 科室列表(含组内成员)。 * UserBase 不可达/学校组不存在时抛业务异常(预览即校验,导入前先给前端看结果)。 */ public List previewGroupDepts(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 + ")"); } List depts = new ArrayList<>(); collectDeptGroups(subtree, depts); List out = new ArrayList<>(); for (BoaGroup dept : depts) { GroupDeptVo vo = new GroupDeptVo(); vo.setDeptKey(String.valueOf(dept.getId())); vo.setDeptName(dept.getName()); try { List members = groupClient.getGroupUsers(dept.getId(), false, null); if (members != null) { for (BoaUser u : members) { if (u != null && u.getId() != null) { vo.getUsers().add(new MemberVo(u.getId(), u.getName() == null ? "" : u.getName())); } } } } catch (Exception e) { log.warn("[flow-center] 组成员拉取失败(groupId={}, name={}):{}", dept.getId(), dept.getName(), e.getMessage()); } out.add(vo); } return out; } /** * 一键导入:按 {@link #previewGroupDepts} 的口径批量落映射;已存在(sch+dept+user 且启用中) * 跳过不重复刷写。空成员的科室保留为无映射科室(流程编辑器可见提示,成员后续手工补)。 */ @Transactional public GroupImportResultVo importGroups(Long schId) { List depts = previewGroupDepts(schId); GroupImportResultVo result = new GroupImportResultVo(); for (GroupDeptVo dept : depts) { for (MemberVo m : dept.getUsers()) { FlwAssigneeMap existing = mapRepository.findBySchIdAndDeptKeyAndUserId( schId, dept.getDeptKey(), m.getUserId()); if (existing != null && existing.getStatus() == 0) { result.setSkippedExisting(result.getSkippedExisting() + 1); continue; } save(schId, dept.getDeptKey(), dept.getDeptName(), m.getUserId(), m.getUserName()); result.setUsers(result.getUsers() + 1); } result.setDepts(result.getDepts() + 1); } return result; } /** * 收集科室组:子树内 type=容器组类型的节点,其**子组**为科室(容器无子组时容器自身即科室)。 * 语义与 svc-hall ServiceItemService.buildDeptTree 逐字对齐(同一份学校组树两种消费)。 */ private void collectDeptGroups(BoaGroup node, List out) { if (node == null) { return; } if (node.getType() != null && node.getType() == deptGroupType) { if (node.getChildren() == null || node.getChildren().isEmpty()) { out.add(node); } else { for (BoaGroup child : node.getChildren()) { if (child != null && child.getId() != null) { out.add(child); } } } return; } if (node.getChildren() != null) { for (BoaGroup child : node.getChildren()) { collectDeptGroups(child, out); } } } private String fetchName(Integer userId) { try { BoaUser user = userClient.getUserById(String.valueOf(userId)); return user == null ? "" : (user.getName() == null ? "" : user.getName()); } catch (Exception e) { return ""; } } }