From 76070ba58a48fec388dea80cbdca94e1e24dcc42 Mon Sep 17 00:00:00 2001 From: luotaiqian <1147642922@qq.com> Date: Thu, 16 Jul 2026 14:40:45 +0800 Subject: [PATCH] init --- .../utils/ComplianceCheckUtil.java | 238 +++++++++++++++--- 1 file changed, 209 insertions(+), 29 deletions(-) diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/utils/ComplianceCheckUtil.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/utils/ComplianceCheckUtil.java index 005d783..4014eaf 100644 --- a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/utils/ComplianceCheckUtil.java +++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/utils/ComplianceCheckUtil.java @@ -342,11 +342,17 @@ public class ComplianceCheckUtil { } /** - * 行业专业能力人员要求校验 + * 行业专业能力人员要求校验(一人一岗) + *

+ * 每个人员(sourcePersonnelId)可能具备多个专业能力,但只能被分配到一个专业能力岗位。 + * 使用最大流算法(Dinic)求解最优分配方案,判断是否能满足所有专业能力的人数要求。 + *

+ * 核心约束:certCapabilityList 中 personnelId == industryProfessionalPersonCos 中 sourcePersonnelId 标识同一人, + * 一个人最多只能占用一个专业能力名额。 * * @param standardsDOList 人员要求标准(按 行业code + 专业能力code 分类的人数要求) - * @param industryProfessionalPersonCos 当前实际人员数(按 行业code + 专业能力code 分类汇总) - * @param certCapabilityList certCapabilityList + * @param industryProfessionalPersonCos 当前实际人员(用于确定参检人员范围) + * @param certCapabilityList 人员证书专业能力信息(提供人员→专业能力映射) * @return Pair key 是否通过 ,value 失败消息(通过时为 null) */ public static Pair doIndustryProfessionalStandardsValid(List standardsDOList @@ -355,38 +361,212 @@ public class ComplianceCheckUtil { return Pair.of(true, null); } - // 当前人员数按 行业code + 专业能力code 分类汇总 - Map currentCountMap = industryProfessionalPersonCos.stream() - .collect(Collectors.groupingBy( - c -> groupKey(c.getIndustryCode(), c.getProfessionalCapabilityCode()), - Collectors.summingLong(c -> c.getPersonNum() == null ? 0L : c.getPersonNum()))); + // 1. 汇总每个专业能力code的要求人数 + Map requiredMap = standardsDOList.stream() + .collect(Collectors.toMap( + IndustryProfessionalStandardsDO::getProfessionalCapabilityCode, + s -> s.getPersonNum() != null ? s.getPersonNum().intValue() : 0, + Integer::sum, + LinkedHashMap::new)); - // 要求按 行业code + 专业能力code 分类汇总,并记录专业能力名称 - StringBuilder failureMsg = new StringBuilder(); - for (IndustryProfessionalStandardsDO s : standardsDOList) { - String key = groupKey(s.getIndustryCode(), s.getProfessionalCapabilityCode()); - Long currentPerson = currentCountMap.getOrDefault(key, 0L); - if (s.getPersonNum() > currentPerson) { - IndustryEnum industryEnum = IndustryEnum.ofCode(s.getIndustryCode()); - ProfessionalCapabilityEnum professionalCapabilityEnum = ProfessionalCapabilityEnum.ofCode(s.getProfessionalCapabilityCode()); - failureMsg.append(industryEnum.getValue()) - .append(":") - .append(professionalCapabilityEnum.getValue()) - .append(" 需要") - .append(s.getPersonNum()) - .append("人,当前") - .append(currentPerson) - .append("人"); + // 2. 获取本行业备案人员集合(确定参检人员范围) + Set filingPersonIds = industryProfessionalPersonCos.stream() + .map(IndustryProfessionalPersonCo::getSourcePersonnelId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 3. 构建人员→可担任专业能力code集合(同一人可能具备多个专业能力) + Map> personCapabilityMap = new HashMap<>(); + for (OrgPersonnelCertCapabilityComplianceCheckCO cert : certCapabilityList) { + Long pid = cert.getPersonnelId(); + if (pid != null && filingPersonIds.contains(pid) && cert.getProfessionalCapabilityCode() != null) { + personCapabilityMap.computeIfAbsent(pid, k -> new HashSet<>()) + .add(cert.getProfessionalCapabilityCode()); } } - return failureMsg.length() > 0 ? Pair.of(false, failureMsg.toString()) : Pair.of(true, null); + + // 4. 一人一岗最优分配 + Map assignedMap = assignPersonnelByMaxFlow(personCapabilityMap, requiredMap); + + // 5. 判断是否所有专业能力都达标,并生成失败消息 + boolean allMet = true; + IndustryEnum industryEnum = IndustryEnum.ofCode(standardsDOList.get(0).getIndustryCode()); + String industryName = industryEnum != null ? industryEnum.getValue() : standardsDOList.get(0).getIndustryCode(); + + StringBuilder failureMsg = new StringBuilder(); + for (Map.Entry entry : requiredMap.entrySet()) { + String capCode = entry.getKey(); + int required = entry.getValue(); + int assigned = assignedMap.getOrDefault(capCode, 0); + if (assigned < required) { + allMet = false; + ProfessionalCapabilityEnum capEnum = ProfessionalCapabilityEnum.ofCode(capCode); + String capName = capEnum != null ? capEnum.getValue() : capCode; + failureMsg.append(industryName) + .append(":") + .append(capName) + .append(" 需要") + .append(required) + .append("人,因一人一岗最多可分配") + .append(assigned) + .append("人;"); + } + } + return allMet ? Pair.of(true, null) : Pair.of(false, failureMsg.toString()); } /** - * 行业code + 专业能力code 分组键 + * 一人一岗最优分配算法(最大流 / Dinic) + *

+ * 每个人员最多分配到一个专业能力岗位,每个岗位有最低人数要求, + * 求解在"一人一岗"约束下各专业能力实际可分配到的人数。 + *

+ * 网络结构: source → 人员节点(cap=1) → 专业能力节点(cap=1) → sink(cap=要求人数) + * + * @param personCapabilityMap 人员→可担任专业能力code集合(同一人可能具备多个专业能力) + * @param requiredMap 专业能力code→要求人数 + * @return 专业能力code→实际分配人数 */ - private static String groupKey(String industryCode, String professionalCapabilityCode) { - return (industryCode == null ? "" : industryCode) + "_" - + (professionalCapabilityCode == null ? "" : professionalCapabilityCode); + private static Map assignPersonnelByMaxFlow(Map> personCapabilityMap + , Map requiredMap) { + List personIds = new ArrayList<>(personCapabilityMap.keySet()); + List capCodes = new ArrayList<>(requiredMap.keySet()); + Map capIndex = new HashMap<>(); + for (int j = 0; j < capCodes.size(); j++) { + capIndex.put(capCodes.get(j), j); + } + + int numPersons = personIds.size(); + int numCaps = capCodes.size(); + int source = 0; + int sink = numPersons + numCaps + 1; + MaxFlow mf = new MaxFlow(sink + 1); + + // source → person (capacity 1: 每人最多分配一个岗位) + for (int i = 0; i < numPersons; i++) { + mf.addEdge(source, 1 + i, 1); + } + // person → capability (capacity 1: 该人可担任该专业能力) + for (int i = 0; i < numPersons; i++) { + Set caps = personCapabilityMap.get(personIds.get(i)); + if (caps != null) { + for (String cap : caps) { + Integer cj = capIndex.get(cap); + if (cj != null) { + mf.addEdge(1 + i, 1 + numPersons + cj, 1); + } + } + } + } + // capability → sink (capacity = 该专业能力要求人数) + int[] capEdgeIds = new int[numCaps]; + for (int j = 0; j < numCaps; j++) { + capEdgeIds[j] = mf.addEdgeReturnId(1 + numPersons + j, sink, requiredMap.get(capCodes.get(j))); + } + + // 求解最大流 + mf.maxFlow(source, sink); + + // 提取每个专业能力的实际分配人数 + Map assignedMap = new LinkedHashMap<>(); + for (int j = 0; j < numCaps; j++) { + assignedMap.put(capCodes.get(j), mf.getFlow(capEdgeIds[j])); + } + return assignedMap; + } + + /** + * Dinic 最大流算法(内部使用) + *

+ * 用于求解"一人一岗"分配问题:将人员分配到专业能力岗位, + * 每人最多分配一个岗位,每个岗位有最低人数要求。 + *

+ * 网络结构: source → 人员节点(cap=1) → 专业能力节点(cap=1) → sink(cap=要求人数) + * 若最大流 = 所有人数要求之和,则存在合法分配方案。 + */ + private static class MaxFlow { + private final int n; + private final List edges; + private final List> adj; + private final int[] level; + private final int[] iter; + + MaxFlow(int n) { + this.n = n; + this.edges = new ArrayList<>(); + this.adj = new ArrayList<>(); + for (int i = 0; i < n; i++) { + adj.add(new ArrayList<>()); + } + this.level = new int[n]; + this.iter = new int[n]; + } + + void addEdge(int from, int to, int cap) { + addEdgeReturnId(from, to, cap); + } + + int addEdgeReturnId(int from, int to, int cap) { + int edgeId = edges.size(); + adj.get(from).add(edgeId); + edges.add(new int[]{to, cap}); + adj.get(to).add(edgeId + 1); + edges.add(new int[]{from, 0}); + return edgeId; + } + + /** + * 获取某条正向边的实际流量 + */ + int getFlow(int edgeId) { + return edges.get(edgeId ^ 1)[1]; + } + + int maxFlow(int s, int t) { + int flow = 0; + while (bfs(s, t)) { + Arrays.fill(iter, 0); + int f; + while ((f = dfs(s, t, Integer.MAX_VALUE)) > 0) { + flow += f; + } + } + return flow; + } + + private boolean bfs(int s, int t) { + Arrays.fill(level, -1); + Queue queue = new ArrayDeque<>(); + level[s] = 0; + queue.add(s); + while (!queue.isEmpty()) { + int v = queue.poll(); + for (int ei : adj.get(v)) { + int[] e = edges.get(ei); + if (e[1] > 0 && level[e[0]] < 0) { + level[e[0]] = level[v] + 1; + queue.add(e[0]); + } + } + } + return level[t] >= 0; + } + + private int dfs(int v, int t, int f) { + if (v == t) return f; + for (; iter[v] < adj.get(v).size(); iter[v]++) { + int ei = adj.get(v).get(iter[v]); + int[] e = edges.get(ei); + if (e[1] > 0 && level[v] < level[e[0]]) { + int d = dfs(e[0], t, Math.min(f, e[1])); + if (d > 0) { + e[1] -= d; + edges.get(ei ^ 1)[1] += d; + return d; + } + } + } + return 0; + } } }