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;
+ }
}
}