package com.sinata.appointment.service;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.sinata.appointment.constants.BizErrorCode;
|
import com.sinata.appointment.constants.MiniUserType;
|
import com.sinata.appointment.dto.DeptCreateRequest;
|
import com.sinata.appointment.dto.DeptTreeNodeVo;
|
import com.sinata.appointment.dto.DeptUpdateRequest;
|
import com.sinata.appointment.entity.MpUser;
|
import com.sinata.appointment.entity.SysDept;
|
import com.sinata.appointment.exception.BizException;
|
import com.sinata.appointment.mapper.MpUserMapper;
|
import com.sinata.appointment.mapper.SysDeptMapper;
|
import com.sinata.appointment.util.UuidAssert;
|
import java.util.ArrayList;
|
import java.util.Comparator;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.util.StringUtils;
|
|
@Service
|
@RequiredArgsConstructor
|
public class DeptManageApplicationService {
|
|
private static final int MAX_LEVEL = 5;
|
|
private final SysDeptMapper sysDeptMapper;
|
private final MpUserMapper mpUserMapper;
|
|
public List<DeptTreeNodeVo> tree() {
|
List<SysDept> all =
|
sysDeptMapper.selectList(
|
new LambdaQueryWrapper<SysDept>()
|
.eq(SysDept::getDelFlag, 0)
|
.orderByAsc(SysDept::getSortOrder)
|
.orderByAsc(SysDept::getId));
|
Map<Long, DeptTreeNodeVo> vos = new HashMap<>();
|
for (SysDept d : all) {
|
DeptTreeNodeVo v = toVo(d);
|
v.setChildren(new ArrayList<>());
|
vos.put(d.getId(), v);
|
}
|
List<DeptTreeNodeVo> roots = new ArrayList<>();
|
for (SysDept d : all) {
|
DeptTreeNodeVo v = vos.get(d.getId());
|
long pid = d.getParentId() == null ? 0L : d.getParentId();
|
if (pid == 0L) {
|
roots.add(v);
|
} else {
|
DeptTreeNodeVo p = vos.get(pid);
|
if (p != null) {
|
p.getChildren().add(v);
|
} else {
|
roots.add(v);
|
}
|
}
|
}
|
sortChildrenRecursive(roots);
|
for (DeptTreeNodeVo r : roots) {
|
assignLevel(r, 1);
|
}
|
return roots;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> create(DeptCreateRequest req) {
|
long pid = req.getParentId() == null ? 0L : req.getParentId();
|
if (pid != 0L) {
|
SysDept parent = loadActive(pid);
|
int parentDepth = depthOf(parent.getId());
|
if (parentDepth >= MAX_LEVEL) {
|
throw new BizException(BizErrorCode.DEPT_MAX_DEPTH, "无法在第五级下再添加下级部门");
|
}
|
if (parentDepth + 1 > MAX_LEVEL) {
|
throw new BizException(BizErrorCode.DEPT_MAX_DEPTH, "部门层级不能超过 " + MAX_LEVEL + " 级");
|
}
|
}
|
SysDept d = new SysDept();
|
d.setParentId(pid);
|
d.setDeptName(trimName(req.getDeptName()));
|
d.setStatus(normalizeStatus(req.getStatus()));
|
d.setSortOrder(0);
|
applyDeptLeaders(d, req.getLeaderIds());
|
sysDeptMapper.insert(d);
|
SysDept fresh = sysDeptMapper.selectById(d.getId());
|
if (fresh == null || !StringUtils.hasText(fresh.getUuid())) {
|
throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "创建后读取部门失败");
|
}
|
Map<String, Object> m = new HashMap<>();
|
m.put("id", fresh.getId());
|
m.put("uuid", fresh.getUuid());
|
return m;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void update(Long id, DeptUpdateRequest req) {
|
SysDept d = loadActiveWithUuid(id, req.getUuid());
|
d.setDeptName(trimName(req.getDeptName()));
|
d.setStatus(normalizeStatus(req.getStatus()));
|
applyDeptLeaders(d, req.getLeaderIds());
|
|
Long oldParent = d.getParentId() == null ? 0L : d.getParentId();
|
if (oldParent == 0L) {
|
sysDeptMapper.updateById(d);
|
return;
|
}
|
long newPid = req.getParentId() != null ? req.getParentId() : oldParent;
|
if (newPid == 0L) {
|
throw new BizException(BizErrorCode.DEPT_PARENT_INVALID, "非一级部门不能改为无上级");
|
}
|
if (newPid != oldParent) {
|
validateReparent(id, newPid);
|
d.setParentId(newPid);
|
}
|
sysDeptMapper.updateById(d);
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void delete(Long id, String uuid) {
|
SysDept d = loadActiveWithUuid(id, uuid);
|
Set<Long> deptScope = collectSubtreeDeptIds(id);
|
long staffCnt =
|
mpUserMapper.selectCount(
|
new LambdaQueryWrapper<MpUser>()
|
.eq(MpUser::getDelFlag, 0)
|
.eq(MpUser::getUserType, "EMPLOYEE")
|
.in(MpUser::getDeptId, deptScope));
|
if (staffCnt > 0) {
|
throw new BizException(BizErrorCode.DEPT_HAS_BOUND_STAFF, "该部门下有关联员工,无法删除");
|
}
|
long childCnt =
|
sysDeptMapper.selectCount(
|
new LambdaQueryWrapper<SysDept>()
|
.eq(SysDept::getParentId, id)
|
.eq(SysDept::getDelFlag, 0));
|
if (childCnt > 0) {
|
throw new BizException(BizErrorCode.DEPT_HAS_CHILDREN, "存在下级部门,无法删除");
|
}
|
int affected = sysDeptMapper.deleteById(d.getId());
|
if (affected <= 0) {
|
throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "删除失败,请刷新后重试");
|
}
|
}
|
|
private void validateReparent(long deptId, long newParentId) {
|
if (newParentId == deptId) {
|
throw new BizException(BizErrorCode.DEPT_REORDER_CYCLE, "上级部门不能为自身");
|
}
|
loadActive(newParentId);
|
if (isUnderAncestor(newParentId, deptId)) {
|
throw new BizException(BizErrorCode.DEPT_REORDER_CYCLE, "上级部门不能为当前部门的下级");
|
}
|
Map<Long, List<SysDept>> byParent = buildChildrenMap();
|
int subtreeH = subtreeHeight(deptId, byParent);
|
int newParentDepth = depthOf(newParentId);
|
if (newParentDepth + subtreeH > MAX_LEVEL) {
|
throw new BizException(BizErrorCode.DEPT_MAX_DEPTH, "调整上级后将超过 " + MAX_LEVEL + " 级限制");
|
}
|
}
|
|
private boolean isUnderAncestor(long nodeId, long ancestorId) {
|
Long cur = nodeId;
|
Set<Long> guard = new HashSet<>();
|
while (cur != null && cur != 0L) {
|
if (Objects.equals(cur, ancestorId)) {
|
return true;
|
}
|
if (!guard.add(cur)) {
|
break;
|
}
|
SysDept n = sysDeptMapper.selectById(cur);
|
if (n == null || n.getDelFlag() != null && n.getDelFlag() == 1) {
|
break;
|
}
|
Long p = n.getParentId();
|
if (p == null || p == 0L) {
|
break;
|
}
|
cur = p;
|
}
|
return false;
|
}
|
|
private void applyDeptLeaders(SysDept dept, List<Long> leaderIds) {
|
List<Long> normalized = normalizeLeaderIds(leaderIds);
|
validateLeaders(normalized);
|
dept.setLeaderIds(joinIds(normalized));
|
}
|
|
private List<Long> normalizeLeaderIds(List<Long> leaderIds) {
|
List<Long> raw = leaderIds == null ? new ArrayList<>() : new ArrayList<>(leaderIds);
|
List<Long> out = new ArrayList<>();
|
Set<Long> seen = new HashSet<>();
|
for (Long id : raw) {
|
if (id == null || id <= 0 || !seen.add(id)) {
|
continue;
|
}
|
out.add(id);
|
}
|
return out;
|
}
|
|
private void validateLeaders(List<Long> leaderIds) {
|
if (leaderIds == null || leaderIds.isEmpty()) {
|
return;
|
}
|
List<MpUser> users =
|
mpUserMapper.selectList(
|
new LambdaQueryWrapper<MpUser>()
|
.eq(MpUser::getDelFlag, 0)
|
.eq(MpUser::getStatus, 1)
|
.eq(MpUser::getUserType, MiniUserType.EMPLOYEE)
|
.in(MpUser::getId, leaderIds));
|
Set<Long> validIds = new HashSet<>();
|
for (MpUser user : users) {
|
validIds.add(user.getId());
|
}
|
for (Long id : leaderIds) {
|
if (!validIds.contains(id)) {
|
throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "部门负责人不存在或已禁用: " + id);
|
}
|
}
|
}
|
|
private int subtreeHeight(long id, Map<Long, List<SysDept>> byParent) {
|
List<SysDept> ch = byParent.getOrDefault(id, List.of());
|
int max = 1;
|
for (SysDept c : ch) {
|
if (c.getDelFlag() != null && c.getDelFlag() == 1) {
|
continue;
|
}
|
max = Math.max(max, 1 + subtreeHeight(c.getId(), byParent));
|
}
|
return max;
|
}
|
|
private Map<Long, List<SysDept>> buildChildrenMap() {
|
List<SysDept> all =
|
sysDeptMapper.selectList(
|
new LambdaQueryWrapper<SysDept>().eq(SysDept::getDelFlag, 0));
|
Map<Long, List<SysDept>> m = new HashMap<>();
|
for (SysDept d : all) {
|
m.computeIfAbsent(d.getParentId() == null ? 0L : d.getParentId(), k -> new ArrayList<>())
|
.add(d);
|
}
|
return m;
|
}
|
|
private Set<Long> collectSubtreeDeptIds(Long rootDeptId) {
|
Set<Long> deptIds = new HashSet<>();
|
if (rootDeptId == null || rootDeptId <= 0) {
|
return deptIds;
|
}
|
List<SysDept> all =
|
sysDeptMapper.selectList(
|
new LambdaQueryWrapper<SysDept>()
|
.eq(SysDept::getDelFlag, 0)
|
.select(SysDept::getId, SysDept::getParentId));
|
Map<Long, List<Long>> children = new HashMap<>();
|
for (SysDept dept : all) {
|
Long parentId = dept.getParentId() == null ? 0L : dept.getParentId();
|
children.computeIfAbsent(parentId, k -> new ArrayList<>()).add(dept.getId());
|
}
|
List<Long> stack = new ArrayList<>();
|
stack.add(rootDeptId);
|
while (!stack.isEmpty()) {
|
Long current = stack.remove(stack.size() - 1);
|
if (!deptIds.add(current)) {
|
continue;
|
}
|
List<Long> next = children.getOrDefault(current, List.of());
|
stack.addAll(next);
|
}
|
return deptIds;
|
}
|
|
/** 节点到根的距离:根为 1 */
|
private int depthOf(long id) {
|
int d = 1;
|
Long cur = id;
|
Set<Long> guard = new HashSet<>();
|
while (cur != null && cur != 0L) {
|
SysDept n = sysDeptMapper.selectById(cur);
|
if (n == null || n.getDelFlag() != null && n.getDelFlag() == 1) {
|
break;
|
}
|
Long p = n.getParentId();
|
if (p == null || p == 0L) {
|
break;
|
}
|
d++;
|
if (!guard.add(cur)) {
|
break;
|
}
|
cur = p;
|
}
|
return d;
|
}
|
|
private void sortChildrenRecursive(List<DeptTreeNodeVo> nodes) {
|
nodes.sort(
|
Comparator.comparing(DeptTreeNodeVo::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder()))
|
.thenComparing(DeptTreeNodeVo::getId));
|
for (DeptTreeNodeVo n : nodes) {
|
sortChildrenRecursive(n.getChildren());
|
}
|
}
|
|
private void assignLevel(DeptTreeNodeVo n, int level) {
|
n.setLevel(level);
|
for (DeptTreeNodeVo c : n.getChildren()) {
|
assignLevel(c, level + 1);
|
}
|
}
|
|
private DeptTreeNodeVo toVo(SysDept d) {
|
DeptTreeNodeVo v = new DeptTreeNodeVo();
|
v.setId(d.getId());
|
v.setUuid(d.getUuid());
|
v.setParentId(d.getParentId());
|
v.setDeptName(d.getDeptName());
|
v.setSortOrder(d.getSortOrder());
|
v.setStatus(d.getStatus() == null ? 1 : d.getStatus());
|
List<Long> leaderIds = parseLeaderIds(d);
|
v.setLeaderIds(leaderIds);
|
v.setLeaderNames(resolveLeaderNames(leaderIds));
|
return v;
|
}
|
|
private List<Long> parseLeaderIds(SysDept dept) {
|
return parseIds(dept.getLeaderIds());
|
}
|
|
private List<Long> parseIds(String raw) {
|
if (!StringUtils.hasText(raw)) {
|
return List.of();
|
}
|
List<Long> ids = new ArrayList<>();
|
Set<Long> seen = new HashSet<>();
|
for (String part : raw.split(",")) {
|
if (!StringUtils.hasText(part)) {
|
continue;
|
}
|
try {
|
Long id = Long.parseLong(part.trim());
|
if (id > 0 && seen.add(id)) {
|
ids.add(id);
|
}
|
} catch (NumberFormatException ignored) {
|
}
|
}
|
return ids;
|
}
|
|
private String joinIds(List<Long> ids) {
|
if (ids == null || ids.isEmpty()) {
|
return null;
|
}
|
return ids.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(","));
|
}
|
|
private String resolveLeaderNames(List<Long> leaderIds) {
|
if (leaderIds == null || leaderIds.isEmpty()) {
|
return null;
|
}
|
List<MpUser> users =
|
mpUserMapper.selectList(
|
new LambdaQueryWrapper<MpUser>()
|
.eq(MpUser::getDelFlag, 0)
|
.in(MpUser::getId, leaderIds));
|
Map<Long, String> names = new HashMap<>();
|
for (MpUser user : users) {
|
names.put(user.getId(), user.getNickName());
|
}
|
List<String> ordered = new ArrayList<>();
|
for (Long id : leaderIds) {
|
String name = names.get(id);
|
if (StringUtils.hasText(name)) {
|
ordered.add(name.trim());
|
}
|
}
|
return ordered.isEmpty() ? null : String.join("、", ordered);
|
}
|
|
private SysDept loadActive(long id) {
|
SysDept d = sysDeptMapper.selectById(id);
|
if (d == null || d.getDelFlag() != null && d.getDelFlag() == 1) {
|
throw new BizException(BizErrorCode.DEPT_PARENT_INVALID, "上级部门不存在");
|
}
|
return d;
|
}
|
|
private SysDept loadActiveWithUuid(long id, String uuid) {
|
SysDept d = sysDeptMapper.selectById(id);
|
if (d == null || d.getDelFlag() != null && d.getDelFlag() == 1) {
|
throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "资源不存在或无权访问");
|
}
|
UuidAssert.match(d, uuid);
|
return d;
|
}
|
|
private static int normalizeStatus(Integer s) {
|
if (s == null || (s != 0 && s != 1)) {
|
throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "部门状态无效");
|
}
|
return s;
|
}
|
|
private static String trimName(String name) {
|
if (!StringUtils.hasText(name)) {
|
throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "部门名称不能为空");
|
}
|
return name.trim();
|
}
|
}
|