longz
2026-07-13 5882ba8863d77fdcc168b176c3ffb30e8a20556a
backend/src/main/java/com/sinata/appointment/service/ApprovalEngineService.java
@@ -17,6 +17,7 @@
import com.sinata.appointment.entity.MpPassLocalUnitMember;
import com.sinata.appointment.entity.MpStaffGroupMember;
import com.sinata.appointment.entity.MpUser;
import com.sinata.appointment.entity.SysDept;
import com.sinata.appointment.exception.BizException;
import com.sinata.appointment.mapper.ApprovalInstanceMapper;
import com.sinata.appointment.mapper.ApprovalTaskMapper;
@@ -25,6 +26,7 @@
import com.sinata.appointment.mapper.MpPassLocalUnitMemberMapper;
import com.sinata.appointment.mapper.MpStaffGroupMemberMapper;
import com.sinata.appointment.mapper.MpUserMapper;
import com.sinata.appointment.mapper.SysDeptMapper;
import com.sinata.appointment.constants.BizErrorCode;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -48,11 +50,13 @@
    private static final TypeReference<List<Long>> LONG_LIST_TYPE = new TypeReference<>() {};
    private static final String AUTO_APPROVE_COMMENT = "前序节点已审批,系统自动同意";
    private static final String LOCAL_UNIT_GROUP_TOKEN = ":UNIT:";
    private final BizApprovalNodeConfigMapper bizApprovalNodeConfigMapper;
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApprovalTaskMapper approvalTaskMapper;
    private final MpUserMapper mpUserMapper;
    private final SysDeptMapper sysDeptMapper;
    private final MpPassLocalUnitMapper mpPassLocalUnitMapper;
    private final MpPassLocalUnitMemberMapper mpPassLocalUnitMemberMapper;
    private final MpStaffGroupMemberMapper mpStaffGroupMemberMapper;
@@ -118,15 +122,15 @@
        }
        if (ApprovalNodeCode.LEVEL2_DEPT_LEADER.equals(nodeCode)) {
            Long deptId = resolveVisitedDeptId(order);
            return listEmployeeIdsByDeptAndLeaderFlag(deptId, true);
            return getDeptLeaderIds(deptId);
        }
        if (ApprovalNodeCode.LEVEL4_VISIT_DEPT.equals(nodeCode)) {
            Long competentDeptId = getVisitCompetentDeptId();
            return listEmployeeIdsByDeptAndLeaderFlag(competentDeptId, false);
            return listEmployeeIdsByDeptWithLeaderFallback(competentDeptId);
        }
        if (ApprovalNodeCode.LEVEL5_VISIT_DEPT_LEADER.equals(nodeCode)) {
            Long competentDeptId = getVisitCompetentDeptId();
            return listEmployeeIdsByDeptAndLeaderFlag(competentDeptId, true);
            return getDeptLeaderIds(competentDeptId);
        }
        if (ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER.equals(nodeCode)) {
            return List.of();
@@ -161,7 +165,59 @@
        }
    }
    private List<Long> listEmployeeIdsByDeptAndLeaderFlag(Long deptId, boolean leaderFlag) {
    private List<Long> getDeptLeaderIds(Long deptId) {
        if (deptId == null || deptId <= 0) {
            return List.of();
        }
        SysDept dept = sysDeptMapper.selectById(deptId);
        if (dept == null || dept.getDelFlag() != null && dept.getDelFlag() == 1) {
            return List.of();
        }
        List<Long> leaderIds = parseDeptLeaderIds(dept);
        if (leaderIds.isEmpty()) {
            return List.of();
        }
        List<MpUser> leaders =
                mpUserMapper.selectList(
                        new LambdaQueryWrapper<MpUser>()
                                .eq(MpUser::getDelFlag, 0)
                                .eq(MpUser::getStatus, 1)
                                .eq(MpUser::getUserType, MiniUserType.EMPLOYEE)
                                .in(MpUser::getId, leaderIds));
        Map<Long, MpUser> leaderMap = new LinkedHashMap<>();
        for (MpUser leader : leaders) {
            leaderMap.put(leader.getId(), leader);
        }
        List<Long> result = new ArrayList<>();
        for (Long id : leaderIds) {
            if (leaderMap.containsKey(id)) {
                result.add(id);
            }
        }
        return result;
    }
    private List<Long> parseDeptLeaderIds(SysDept dept) {
        List<Long> ids = new ArrayList<>();
        Set<Long> seen = new LinkedHashSet<>();
        if (StringUtils.hasText(dept.getLeaderIds())) {
            for (String part : dept.getLeaderIds().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 List<Long> listAllActiveEmployeeIdsByDept(Long deptId) {
        if (deptId == null || deptId <= 0) {
            return List.of();
        }
@@ -171,9 +227,12 @@
                                .eq(MpUser::getDelFlag, 0)
                                .eq(MpUser::getStatus, 1)
                                .eq(MpUser::getUserType, MiniUserType.EMPLOYEE)
                                .eq(MpUser::getDeptId, deptId)
                                .eq(MpUser::getDeptLeaderFlag, leaderFlag));
        return users.stream().map(MpUser::getId).filter(Objects::nonNull).toList();
                                .eq(MpUser::getDeptId, deptId));
        return users.stream().map(MpUser::getId).filter(Objects::nonNull).distinct().toList();
    }
    private List<Long> listEmployeeIdsByDeptWithLeaderFallback(Long deptId) {
        return listAllActiveEmployeeIdsByDept(deptId);
    }
    @Transactional(rollbackFor = Exception.class)
@@ -252,7 +311,7 @@
        inst.setStatus(ApprovalInstanceStatus.REJECTED);
        inst.setFinishedAt(LocalDateTime.now());
        approvalInstanceMapper.updateById(inst);
        List<ApprovalTask> pending =
        List<ApprovalTask> remainingTasks =
                approvalTaskMapper.selectList(
                        new LambdaQueryWrapper<ApprovalTask>()
                                .eq(ApprovalTask::getInstanceId, inst.getId())
@@ -260,9 +319,9 @@
                                        ApprovalTask::getTaskStatus,
                                        ApprovalTaskStatus.PENDING,
                                        ApprovalTaskStatus.WAITING));
        for (ApprovalTask t : pending) {
        for (ApprovalTask t : remainingTasks) {
            t.setTaskStatus(ApprovalTaskStatus.REJECTED);
            t.setComment(reason);
            t.setComment(null);
            approvalTaskMapper.updateById(t);
        }
    }
@@ -380,94 +439,16 @@
    }
    private int createTasksForLocalUnitNode(Long instanceId, AppointmentOrder order, String initialStatus) {
        List<Long> localUnitIds = parseLongJsonArray(order.getPassLocalUnitIds());
        if (localUnitIds.isEmpty()) {
            return 0;
        }
        List<MpPassLocalUnit> units =
                mpPassLocalUnitMapper.selectList(
                        new LambdaQueryWrapper<MpPassLocalUnit>()
                                .eq(MpPassLocalUnit::getDelFlag, 0)
                                .in(MpPassLocalUnit::getId, localUnitIds));
        if (units.isEmpty()) {
            return 0;
        }
        Map<Long, MpPassLocalUnit> unitMap =
                units.stream().collect(java.util.stream.Collectors.toMap(MpPassLocalUnit::getId, x -> x, (a, b) -> a));
        LinkedHashSet<Long> deptIds = new LinkedHashSet<>();
        LinkedHashSet<Long> groupIds = new LinkedHashSet<>();
        LinkedHashMap<String, List<Long>> tempGroups = new LinkedHashMap<>();
        for (Long unitId : localUnitIds) {
            MpPassLocalUnit unit = unitMap.get(unitId);
            if (unit == null || unit.getApproverType() == null) {
                continue;
            }
            if (unit.getApproverType() == LocalUnitApproverType.DEPT) {
                for (Long deptId : parseLongJsonArray(unit.getApproverDeptIds())) {
                    if (deptId != null && deptId > 0) {
                        deptIds.add(deptId);
                    }
                }
                continue;
            }
            if (unit.getApproverType() == LocalUnitApproverType.STAFF_GROUP) {
                Long gid = unit.getApproverGroupId();
                if (gid != null && gid > 0) {
                    groupIds.add(gid);
                }
                continue;
            }
            if (unit.getApproverType() == LocalUnitApproverType.STAFF) {
                List<MpPassLocalUnitMember> members =
                        mpPassLocalUnitMemberMapper.selectList(
                                new LambdaQueryWrapper<MpPassLocalUnitMember>()
                                        .eq(MpPassLocalUnitMember::getDelFlag, 0)
                                        .eq(MpPassLocalUnitMember::getUnitId, unitId)
                                        .orderByAsc(MpPassLocalUnitMember::getId));
                LinkedHashSet<Long> mids = new LinkedHashSet<>();
                for (MpPassLocalUnitMember m : members) {
                    if (m.getMpUserId() != null && m.getMpUserId() > 0) {
                        mids.add(m.getMpUserId());
                    }
                }
                if (mids.isEmpty()) {
                    continue;
                }
                List<Long> ordered = mids.stream().toList();
                String sign = buildTempGroupSign(ordered);
                tempGroups.putIfAbsent(sign, ordered);
            }
        }
        List<LocalUnitApprovalGroup> groups = resolveLocalUnitApprovalGroups(order, false);
        int total = 0;
        for (Long deptId : deptIds) {
            List<Long> assignees = listEmployeeIdsByDeptAndLeaderFlag(deptId, true);
            String key = ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER + ":DEPT:" + deptId;
            total += createTasksWithGroupKey(instanceId, ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER, key, assignees, initialStatus);
        }
        for (Long groupId : groupIds) {
            List<MpStaffGroupMember> members =
                    mpStaffGroupMemberMapper.selectList(
                            new LambdaQueryWrapper<MpStaffGroupMember>()
                                    .eq(MpStaffGroupMember::getDelFlag, 0)
                                    .eq(MpStaffGroupMember::getGroupId, groupId)
                                    .orderByAsc(MpStaffGroupMember::getId));
            LinkedHashSet<Long> mids = new LinkedHashSet<>();
            for (MpStaffGroupMember m : members) {
                if (m.getMpUserId() != null && m.getMpUserId() > 0) {
                    mids.add(m.getMpUserId());
                }
            }
            List<Long> assignees = filterActiveEmployeeIds(mids.stream().toList());
            String key = ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER + ":GROUP:" + groupId;
            total += createTasksWithGroupKey(instanceId, ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER, key, assignees, initialStatus);
        }
        for (Map.Entry<String, List<Long>> e : tempGroups.entrySet()) {
            List<Long> assignees = filterActiveEmployeeIds(e.getValue());
            String key = ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER + ":TMP:" + e.getKey();
            total += createTasksWithGroupKey(instanceId, ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER, key, assignees, initialStatus);
        for (LocalUnitApprovalGroup group : groups) {
            total +=
                    createTasksWithGroupKey(
                            instanceId,
                            ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER,
                            buildLocalUnitGroupKey(group.unitId()),
                            group.assigneeIds(),
                            initialStatus);
        }
        return total;
    }
@@ -493,122 +474,15 @@
                    "节点无审批人: " + resolveNodeName(ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER));
        }
        List<MpPassLocalUnit> units =
                mpPassLocalUnitMapper.selectList(
                        new LambdaQueryWrapper<MpPassLocalUnit>()
                                .eq(MpPassLocalUnit::getDelFlag, 0)
                                .in(MpPassLocalUnit::getId, localUnitIds));
        if (units.size() != localUnitIds.size()) {
            throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位配置缺失,无法生成审批人");
        }
        Map<Long, MpPassLocalUnit> unitMap =
                units.stream()
                        .collect(
                                java.util.stream.Collectors.toMap(
                                        MpPassLocalUnit::getId, x -> x, (a, b) -> a));
        LinkedHashSet<Long> deptIds = new LinkedHashSet<>();
        LinkedHashSet<Long> groupIds = new LinkedHashSet<>();
        LinkedHashMap<String, List<Long>> tempGroups = new LinkedHashMap<>();
        for (Long unitId : localUnitIds) {
            MpPassLocalUnit unit = unitMap.get(unitId);
            if (unit == null || unit.getApproverType() == null) {
                throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批类型缺失: " + unitId);
            }
            if (unit.getApproverType() == LocalUnitApproverType.DEPT) {
                List<Long> ids = parseLongJsonArray(unit.getApproverDeptIds());
                if (ids.isEmpty()) {
                    throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批部门未配置: " + unitId);
                }
                deptIds.addAll(ids);
                continue;
            }
            if (unit.getApproverType() == LocalUnitApproverType.STAFF_GROUP) {
                Long gid = unit.getApproverGroupId();
                if (gid == null || gid <= 0) {
                    throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批分组未配置: " + unitId);
                }
                groupIds.add(gid);
                continue;
            }
            if (unit.getApproverType() == LocalUnitApproverType.STAFF) {
                List<MpPassLocalUnitMember> members =
                        mpPassLocalUnitMemberMapper.selectList(
                                new LambdaQueryWrapper<MpPassLocalUnitMember>()
                                        .eq(MpPassLocalUnitMember::getDelFlag, 0)
                                        .eq(MpPassLocalUnitMember::getUnitId, unitId)
                                        .orderByAsc(MpPassLocalUnitMember::getId));
                LinkedHashSet<Long> mids = new LinkedHashSet<>();
                for (MpPassLocalUnitMember m : members) {
                    if (m.getMpUserId() != null && m.getMpUserId() > 0) {
                        mids.add(m.getMpUserId());
                    }
                }
                if (mids.isEmpty()) {
                    throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批人员未配置: " + unitId);
                }
                List<Long> ordered = mids.stream().toList();
                String sign = buildTempGroupSign(ordered);
                tempGroups.putIfAbsent(sign, ordered);
                continue;
            }
            throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批类型无效: " + unitId);
        }
        List<LocalUnitApprovalGroup> groups = resolveLocalUnitApprovalGroups(order, true);
        int total = 0;
        for (Long deptId : deptIds) {
            List<Long> assignees = listEmployeeIdsByDeptAndLeaderFlag(deptId, true);
            if (assignees.isEmpty()) {
                throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "审批部门无负责人: " + deptId);
            }
            String key = ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER + ":DEPT:" + deptId;
        for (LocalUnitApprovalGroup group : groups) {
            total +=
                    createTasksWithGroupKey(
                            instanceId,
                            ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER,
                            key,
                            assignees,
                            initialStatus);
        }
        for (Long groupId : groupIds) {
            List<MpStaffGroupMember> members =
                    mpStaffGroupMemberMapper.selectList(
                            new LambdaQueryWrapper<MpStaffGroupMember>()
                                    .eq(MpStaffGroupMember::getDelFlag, 0)
                                    .eq(MpStaffGroupMember::getGroupId, groupId)
                                    .orderByAsc(MpStaffGroupMember::getId));
            LinkedHashSet<Long> mids = new LinkedHashSet<>();
            for (MpStaffGroupMember m : members) {
                if (m.getMpUserId() != null && m.getMpUserId() > 0) {
                    mids.add(m.getMpUserId());
                }
            }
            List<Long> assignees = filterActiveEmployeeIds(mids.stream().toList());
            if (assignees.isEmpty()) {
                throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "审批分组无有效成员: " + groupId);
            }
            String key = ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER + ":GROUP:" + groupId;
            total +=
                    createTasksWithGroupKey(
                            instanceId,
                            ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER,
                            key,
                            assignees,
                            initialStatus);
        }
        for (Map.Entry<String, List<Long>> e : tempGroups.entrySet()) {
            List<Long> assignees = filterActiveEmployeeIds(e.getValue());
            if (assignees.isEmpty()) {
                throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "临时审批组无有效成员");
            }
            String key = ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER + ":TMP:" + e.getKey();
            total +=
                    createTasksWithGroupKey(
                            instanceId,
                            ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER,
                            key,
                            assignees,
                            buildLocalUnitGroupKey(group.unitId()),
                            group.assigneeIds(),
                            initialStatus);
        }
        if (total <= 0) {
@@ -617,6 +491,125 @@
                    "节点无审批人: " + resolveNodeName(ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER));
        }
        return total;
    }
    private List<LocalUnitApprovalGroup> resolveLocalUnitApprovalGroups(
            AppointmentOrder order, boolean strict) {
        List<Long> localUnitIds = parseLongJsonArray(order.getPassLocalUnitIds());
        if (localUnitIds.isEmpty()) {
            return List.of();
        }
        List<MpPassLocalUnit> units =
                mpPassLocalUnitMapper.selectList(
                        new LambdaQueryWrapper<MpPassLocalUnit>()
                                .eq(MpPassLocalUnit::getDelFlag, 0)
                                .in(MpPassLocalUnit::getId, localUnitIds));
        Map<Long, MpPassLocalUnit> unitMap =
                units.stream()
                        .collect(
                                java.util.stream.Collectors.toMap(
                                        MpPassLocalUnit::getId, x -> x, (a, b) -> a));
        if (strict && unitMap.size() != localUnitIds.size()) {
            throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位配置缺失,无法生成审批人");
        }
        List<LocalUnitApprovalGroup> groups = new ArrayList<>();
        for (Long unitId : localUnitIds) {
            MpPassLocalUnit unit = unitMap.get(unitId);
            if (unit == null) {
                if (strict) {
                    throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位配置缺失,无法生成审批人");
                }
                continue;
            }
            List<Long> assignees = resolveLocalUnitAssignees(unitId, unit, strict);
            if (assignees.isEmpty()) {
                continue;
            }
            groups.add(new LocalUnitApprovalGroup(unitId, assignees));
        }
        return groups;
    }
    private List<Long> resolveLocalUnitAssignees(Long unitId, MpPassLocalUnit unit, boolean strict) {
        Integer approverType = unit.getApproverType();
        if (approverType == null) {
            if (strict) {
                throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批类型缺失: " + unitId);
            }
            return List.of();
        }
        if (approverType == LocalUnitApproverType.DEPT) {
            List<Long> deptIds = parseLongJsonArray(unit.getApproverDeptIds());
            if (deptIds.isEmpty()) {
                if (strict) {
                    throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批部门未配置: " + unitId);
                }
                return List.of();
            }
            LinkedHashSet<Long> assignees = new LinkedHashSet<>();
            for (Long deptId : deptIds) {
                List<Long> deptApprovers = listAllActiveEmployeeIdsByDept(deptId);
                if (strict && deptApprovers.isEmpty()) {
                    throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "审批部门无可用审批人: " + deptId);
                }
                assignees.addAll(deptApprovers);
            }
            return assignees.stream().toList();
        }
        if (approverType == LocalUnitApproverType.STAFF_GROUP) {
            Long groupId = unit.getApproverGroupId();
            if (groupId == null || groupId <= 0) {
                if (strict) {
                    throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批分组未配置: " + unitId);
                }
                return List.of();
            }
            List<MpStaffGroupMember> members =
                    mpStaffGroupMemberMapper.selectList(
                            new LambdaQueryWrapper<MpStaffGroupMember>()
                                    .eq(MpStaffGroupMember::getDelFlag, 0)
                                    .eq(MpStaffGroupMember::getGroupId, groupId)
                                    .orderByAsc(MpStaffGroupMember::getId));
            LinkedHashSet<Long> mids = new LinkedHashSet<>();
            for (MpStaffGroupMember member : members) {
                if (member.getMpUserId() != null && member.getMpUserId() > 0) {
                    mids.add(member.getMpUserId());
                }
            }
            List<Long> assignees = filterActiveEmployeeIds(mids.stream().toList());
            if (strict && assignees.isEmpty()) {
                throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "审批分组无有效成员: " + groupId);
            }
            return assignees;
        }
        if (approverType == LocalUnitApproverType.STAFF) {
            List<MpPassLocalUnitMember> members =
                    mpPassLocalUnitMemberMapper.selectList(
                            new LambdaQueryWrapper<MpPassLocalUnitMember>()
                                    .eq(MpPassLocalUnitMember::getDelFlag, 0)
                                    .eq(MpPassLocalUnitMember::getUnitId, unitId)
                                    .orderByAsc(MpPassLocalUnitMember::getId));
            LinkedHashSet<Long> mids = new LinkedHashSet<>();
            for (MpPassLocalUnitMember member : members) {
                if (member.getMpUserId() != null && member.getMpUserId() > 0) {
                    mids.add(member.getMpUserId());
                }
            }
            List<Long> assignees = filterActiveEmployeeIds(mids.stream().toList());
            if (strict && assignees.isEmpty()) {
                throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批人员未配置: " + unitId);
            }
            return assignees;
        }
        if (strict) {
            throw new BizException(BizErrorCode.CONFIG_NOT_FOUND, "属地单位审批类型无效: " + unitId);
        }
        return List.of();
    }
    private String buildLocalUnitGroupKey(Long unitId) {
        return ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER + LOCAL_UNIT_GROUP_TOKEN + unitId;
    }
    private String resolveNodeName(String nodeCode) {
@@ -743,11 +736,5 @@
        return out;
    }
    private String buildTempGroupSign(List<Long> memberIds) {
        String joined = memberIds.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining("-"));
        if (joined.length() <= 40) {
            return joined;
        }
        return Integer.toHexString(joined.hashCode());
    }
    private record LocalUnitApprovalGroup(Long unitId, List<Long> assigneeIds) {}
}