diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/PersonnelCertFilingCheckCO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/PersonnelCertFilingCheckCO.java new file mode 100644 index 00000000..6f6cb60a --- /dev/null +++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/PersonnelCertFilingCheckCO.java @@ -0,0 +1,37 @@ +package org.qinan.safetyeval.client.co; + +import lombok.Data; + +/** + * 人员证书 检查 + * + * @author safety-eval + */ +@Data +public class PersonnelCertFilingCheckCO { + /** + * 人员id + */ + private Long id; + + /** + * 人员id + */ + private Long personnelId; + + /** + * 专业能力证书类型编码 + */ + private String professionalCapabilityCode; + + /** + * 证书类型编码 + */ + private String certTypeCode; + + /** + * 证书等级 + */ + private String certLevel; + +} diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/QualFilingPersonnelInfoCO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/QualFilingPersonnelInfoCO.java index aa05f050..ed856b1d 100644 --- a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/QualFilingPersonnelInfoCO.java +++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/QualFilingPersonnelInfoCO.java @@ -49,11 +49,11 @@ public class QualFilingPersonnelInfoCO { @ApiModelProperty(value = "是否注册安全工程师(1是2否)") private Integer registerEngineerFlag; - @ApiModelProperty(value = "是否技术负责人(0否1是)") - private Integer technicalDirectorFlag; + @ApiModelProperty(value = "是否技术负责人") + private Boolean technicalDirectorFlag; - @ApiModelProperty(value = "是否过程控制负责人(0否1是)") - private Integer processControlLeaderFlag; + @ApiModelProperty(value = "是否过程控制负责人") + private Boolean processControlLeaderFlag; @ApiModelProperty(value = "人员岗位类型(TECHNICAL_LEADER=>技术负责人; PROCESS_CONTROL_LEADER=>过程控制负责人)") private String personnelPositionType; diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/dto/QualFilingPersonnelFilingCheckDTO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/dto/QualFilingPersonnelFilingCheckDTO.java index 2103dd9d..cc624c75 100644 --- a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/dto/QualFilingPersonnelFilingCheckDTO.java +++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/dto/QualFilingPersonnelFilingCheckDTO.java @@ -2,6 +2,8 @@ package org.qinan.safetyeval.client.dto; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import org.qinan.safetyeval.client.co.PersonnelCertFilingCheckCO; +import org.qinan.safetyeval.client.co.PersonnelCertFilingPageCO; import org.qinan.safetyeval.domain.entity.CapabilityAssessmentDTO; import org.qinan.safetyeval.domain.entity.TitleCodeCo; @@ -23,7 +25,6 @@ public class QualFilingPersonnelFilingCheckDTO { @ApiModelProperty(value = "能力认定") private List capabilityAssessment; - @ApiModelProperty(value = "是否技术负责人(false否true是)") private Boolean technicalDirectorFlag; @@ -33,6 +34,6 @@ public class QualFilingPersonnelFilingCheckDTO { @ApiModelProperty(value = "职称(多个逗号分隔)") private List titleCodeArr; -// @ApiModelProperty(value = "人员证书信息") -// private List personnelCertFilingPageCOList; + @ApiModelProperty(value = "人员证书信息") + private List personnelCertFilingCheckCOList; } diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/constant/basic/IndustryEnum.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/constant/basic/IndustryEnum.java index e174beda..8e69e6ef 100644 --- a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/constant/basic/IndustryEnum.java +++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/constant/basic/IndustryEnum.java @@ -44,4 +44,16 @@ public enum IndustryEnum { } return null; } + + public static String getValueByCode(String code) { + if (code == null) { + return null; + } + for (IndustryEnum item : values()) { + if (item.code.equals(code)) { + return item.getValue(); + } + } + return null; + } } diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/OrgPersonnelCertMapper.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/OrgPersonnelCertMapper.java index 12de8e6a..389f9c11 100644 --- a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/OrgPersonnelCertMapper.java +++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/OrgPersonnelCertMapper.java @@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.qinan.safetyeval.client.co.OrgPersonnelCertCapabilityComplianceCheckCO; import org.qinan.safetyeval.client.co.OrgPersonnelCertDetailCO; +import org.qinan.safetyeval.client.co.PersonnelCertFilingCheckCO; import org.qinan.safetyeval.infrastructure.dataobject.OrgPersonnelCertDO; import java.util.List; @@ -21,6 +22,14 @@ public interface OrgPersonnelCertMapper extends BaseMapper { List selectCapabilityByPersonnelIds( @Param("personnelIds") List personnelIds); + /** + * 根据人员id列表查询人员证书信息 + * + * @param personnelIds 人员id列表 + * @return 人员证书列表 + */ + List selectCertByPersonnelIds(@Param("personnelIds") List personnelIds); + /** * 根据主键ID查询人员证书详情(关联industry_professional_standards获取专业能力名称) * 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 cbfb2de0..890a6bb1 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 @@ -1,6 +1,7 @@ package org.qinan.safetyeval.infrastructure.utils; import cn.hutool.core.lang.Pair; +import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -8,6 +9,7 @@ import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.qinan.safetyeval.client.co.IndustryProfessionalPersonCo; import org.qinan.safetyeval.client.co.OrgPersonnelCertCapabilityComplianceCheckCO; +import org.qinan.safetyeval.client.co.PersonnelCertFilingCheckCO; import org.qinan.safetyeval.client.co.QualFilingComplianceCheckResultCO; import org.qinan.safetyeval.client.dto.*; import org.qinan.safetyeval.domain.constant.*; @@ -191,9 +193,32 @@ public class ComplianceCheckUtil { .collect(Collectors.joining(CommonUtil.COMMA)); throw new BizException(ErrorCode.UNKNOWN_ERROR, "人员信息不存在" + collect); } + + setPersonnelCert(result); return result; } + /** + * setPersonnelCert + * + * @param result result + */ + private static void setPersonnelCert(List result) { + List personnelIds = result.stream() + .map(QualFilingPersonnelFilingCheckDTO::getId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + List certList = SpringUtil.getBean(OrgPersonnelCertMapper.class) + .selectCertByPersonnelIds(personnelIds); + Map> certMap = certList.stream() + .collect(Collectors.groupingBy(PersonnelCertFilingCheckCO::getPersonnelId)); + for (QualFilingPersonnelFilingCheckDTO dto : result) { + dto.setPersonnelCertFilingCheckCOList(certMap.getOrDefault(dto.getId(), Collections.emptyList())); + } + } + /** * 将人员备案检查数据中的职称信息回填到备案人员列表。 * @@ -232,7 +257,7 @@ public class ComplianceCheckUtil { boolean pass = filing.getFixedAssetAmount() != null && filing.getFixedAssetAmount() >= 800L; String currentValue = filing.getFixedAssetAmount() != null - ? filing.getFixedAssetAmount() + " 万元" : "未填写" ; + ? filing.getFixedAssetAmount() + " 万元" : "未填写"; return buildResult(QualFilingComplianceCheckEnum.FIXED_AMOUNT, pass ? QualFilingComplianceCheckEnum.STATUS_PASS : QualFilingComplianceCheckEnum.STATUS_FAIL, currentValue); @@ -245,9 +270,9 @@ public class ComplianceCheckUtil { boolean pass = filing.getWorkplaceArea() != null && filing.getWorkplaceArea() .compareTo(new BigDecimal(QualFilingComplianceCheckEnum.WORKPLACE_LOWER)) >= 0; - String workplace = filing.getWorkplaceArea() != null ? filing.getWorkplaceArea().toPlainString() : "未填写" ; - String archive = filing.getArchiveRoomArea() != null ? filing.getArchiveRoomArea().toPlainString() : "未填写" ; - String currentValue = "工作场所 " + workplace + " 平方米,档案室 " + archive + " 平方米" ; + String workplace = filing.getWorkplaceArea() != null ? filing.getWorkplaceArea().toPlainString() : "未填写"; + String archive = filing.getArchiveRoomArea() != null ? filing.getArchiveRoomArea().toPlainString() : "未填写"; + String currentValue = "工作场所 " + workplace + " 平方米,档案室 " + archive + " 平方米"; return buildResult(QualFilingComplianceCheckEnum.WORKPLACE, pass ? QualFilingComplianceCheckEnum.STATUS_PASS : QualFilingComplianceCheckEnum.STATUS_FAIL, currentValue); @@ -260,7 +285,7 @@ public class ComplianceCheckUtil { int scopeCount = filing.getBusinessScope() != null ? filing.getBusinessScope().size() : 0; int total = personnelList == null ? 0 : personnelList.size(); boolean pass = total >= QualFilingComplianceCheckEnum.PERSONNEL_COUNT_LOWER + (scopeCount - 1) * 5; - String currentValue = scopeCount + " 个业务范围," + total + " 人" ; + String currentValue = scopeCount + " 个业务范围," + total + " 人"; return buildResult(QualFilingComplianceCheckEnum.PERSONNEL_COUNT, pass ? QualFilingComplianceCheckEnum.STATUS_PASS : QualFilingComplianceCheckEnum.STATUS_FAIL, currentValue); @@ -277,16 +302,22 @@ public class ComplianceCheckUtil { QualFilingComplianceCheckEnum.STATUS_FAIL, "无人员数据"); } int total = personnelList.size(); + String code = CertTypeCodeEnum.REGISTERED_SAFETY_ENGINEER.getCode(); + + // 中/高 级注册安全工程师 Set engineerSet = personnelCertCoList.stream() -// .filter(p -> CertTypeCodeEnum.REGISTERED_SAFETY_ENGINEER -// .getCode().equals(p.getCertTypeCode()) && !CertLevelEnum.JUNIOR.getCode().equals(p.getCertLevel())) - .map(QualFilingPersonnelFilingCheckDTO::getId) + .flatMap(it -> it.getPersonnelCertFilingCheckCOList().stream()) + .filter(it -> code.equals(it.getCertTypeCode())) + .map(PersonnelCertFilingCheckCO::getPersonnelId) .collect(Collectors.toSet()); + + // 高级职称人员 Set seniorTitleSet = personnelList.stream() .filter(p -> p.getTitleCodeArr() != null && p.getTitleCodeArr().stream() .anyMatch(t -> QualificationLevelEnum.SENIOR.getCode().equals(t.getTitleCode()))) .map(QualFilingPersonnelAddCmd::getSourcePersonnelId) .collect(Collectors.toSet()); + long engineerRate = engineerSet.size() * 100L / total; long seniorTitleRate = seniorTitleSet.size() * 100L / total; engineerSet.addAll(seniorTitleSet); @@ -294,24 +325,14 @@ public class ComplianceCheckUtil { boolean pass = engineerRate >= QualFilingComplianceCheckEnum.PERSONNEL_RATE && seniorTitleRate >= QualFilingComplianceCheckEnum.PERSONNEL_RATE && sumRate >= 60; - String currentValue = "注册安工程师 " + engineerRate + "%,高级职称 " + seniorTitleRate + "%,合计 " + sumRate + "%" ; + String currentValue = "注册安工程师 " + engineerRate + "%,高级职称 " + seniorTitleRate + "%,合计 " + sumRate + "%"; if (!pass && throwException) { throw new BizException(PERSON_NOT_ENOUGH, currentValue); } - // 根据人员id查询专业能力code并设置到CO中 - List sourcePersonnelIds = personnelList.stream() - .map(QualFilingPersonnelAddCmd::getSourcePersonnelId) - .distinct() - .collect(Collectors.toList()); - List personnelCoList = SpringUtil.getBean(OrgPersonnelMapper.class) - .selectProfessionalCapabilityByPersonnelIds(sourcePersonnelIds); - Pair booleanStringPair = industryProfessionalStandardsValid( - businessScope, personnelCoList, null); - - /*Pair booleanStringPair = industryProfessionalStandardsValid( - businessScope, personnelCoList, personnelCertCoList);*/ - + // 安全评价机构业务范围与专业能力配备标准 校验 + Pair booleanStringPair = + industryProfessionalStandardsPersonValid(businessScope, personnelCertCoList); boolean personPassed = Boolean.TRUE.equals(booleanStringPair.getKey()); if (!personPassed && throwException) { throw new BizException(PERSON_NOT_ENOUGH, booleanStringPair.getValue()); @@ -375,7 +396,7 @@ public class ComplianceCheckUtil { if (industryEnum != null) { scope = industryEnum.getValue(); } else { - scope = "未知行业范围" ; + scope = "未知行业范围"; } currentValue.append("业务[").append(scope).append("] 技术负责人 ") .append(technicalDirectorCount).append("/1,过程控制负责人 ") @@ -413,7 +434,7 @@ public class ComplianceCheckUtil { && m.getUploadStatusCode() == QualFilingMaterialUploadStatusEnum.UPLOADED.getCode() && m.getMaterialType() != null && m.getMaterialType() == MaterialTypeEnum.NO_VIOLATION_RECORD.getCode()); - String currentValue = hasRecord ? "未命中重大违法失信记录" : "命中重大违法失信记录" ; + String currentValue = hasRecord ? "未命中重大违法失信记录" : "命中重大违法失信记录"; return buildResult(QualFilingComplianceCheckEnum.CREDIT_RECORD, hasRecord ? QualFilingComplianceCheckEnum.STATUS_PASS : QualFilingComplianceCheckEnum.STATUS_FAIL, currentValue); @@ -439,19 +460,15 @@ public class ComplianceCheckUtil { } /** - * industryProfessionalStandardsValid + * 安全评价机构业务范围与专业能力配备标准 校验 * * @param industryCodeList industryCodeList - * @param personnelCoList industryProfessionalPersonCos * @param personnelCertCoList certCapabilityList * @return Pair key 是否通过 ,value 失败消息(通过时为 null) */ - public static Pair industryProfessionalStandardsValid(List industryCodeList - , List personnelCoList - , List personnelCertCoList) { - if (CollectionUtils.isEmpty(industryCodeList)) { - return Pair.of(false, "没有业务范围"); - } + public static Pair industryProfessionalStandardsPersonValid(List industryCodeList + , List personnelCertCoList) { + // 通过 SpringUtil 获取 Mapper,根据 industryCodeList 查询专业能力配备标准 IndustryProfessionalStandardsMapper mapper = SpringUtil.getBean(IndustryProfessionalStandardsMapper.class); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); @@ -460,29 +477,145 @@ public class ComplianceCheckUtil { List standardsDOList = mapper.selectList(queryWrapper); if (CollectionUtils.isEmpty(standardsDOList)) { - log.warn("没有配置标准"); - return Pair.of(true, null); + log.warn("务范围与专业能力配备标准未开启"); + return Pair.of(false, "务范围与专业能力配备标准未开启"); } // 按 industryCode 分组后循环调用 doIndustryProfessionalStandardsValid Map> standardsGroupByIndustry = standardsDOList.stream() .collect(Collectors.groupingBy(IndustryProfessionalStandardsDO::getIndustryCode)); + String notExistIndustryCodeList = industryCodeList.stream().filter(it -> !standardsGroupByIndustry.containsKey(it)) + .map(IndustryEnum::getValueByCode) + .collect(Collectors.joining(CommonUtil.COMMA)); + if (StrUtil.isNotBlank(notExistIndustryCodeList)) { + return Pair.of(false, "务范围 " + notExistIndustryCodeList + "与专业能力配备标准未开启"); + } - List personnelCoCopyList = new ArrayList<>(personnelCoList); - Map> personnelCertCoGroupByIndustry = - personnelCertCoList.stream().collect( - Collectors.groupingBy(OrgPersonnelCertCapabilityComplianceCheckCO::getBusinessScope)); + return doIndustryProfessionalStandardsValid(standardsGroupByIndustry, personnelCertCoList); + } - StringJoiner failureMsg = new StringJoiner(";"); - for (Map.Entry> entry : standardsGroupByIndustry.entrySet()) { - Pair result = doIndustryProfessionalStandardsValid(entry.getValue(), personnelCoCopyList - , personnelCertCoGroupByIndustry.getOrDefault(entry.getKey(), new ArrayList<>())); - if (Boolean.FALSE.equals(result.getKey()) && result.getValue() != null) { - failureMsg.add(result.getValue()); + + /** + * 安全评价机构业务范围与专业能力配备标准 校验 + *

+ * 核心约束:一人只能代表一个专业,申请多个业务范围时人员不能重复计算。 + * 分两阶段校验: + *

    + *
  • 阶段一(逐行业计数):逐个行业检查所需要的能力人员是否充足,不足即记录错误并提前返回。
  • + *
  • 阶段二(全局最优分配):所有行业都通过后,使用最大流算法(Dinic)在 + * "一人一专业、多业务范围不重复计算" 的约束下,判断是否存在全局分配方案满足所有需求。
  • + *
+ * + * @param standardsGroupByIndustryMap 是标准的行业要求 枚举类 IndustryEnum + * @param personnelCertCoList 人员的能力在 capabilityAssessment.professionalCapabilityCode + * @return Pair key 是否通过 ,value 失败消息(通过时为 null) + */ + public static Pair doIndustryProfessionalStandardsValid( + Map> standardsGroupByIndustryMap + , List personnelCertCoList) { + + // 1. 收集人员 → 具备的专业能力code集合(来源于能力认定) + Map> personCapabilityMap = personnelCertCoList.stream() + .filter(p -> !CollectionUtils.isEmpty(p.getCapabilityAssessment())) + .collect(Collectors.toMap( + QualFilingPersonnelFilingCheckDTO::getId, + p -> p.getCapabilityAssessment().stream() + .map(CapabilityAssessmentDTO::getProfessionalCapabilityCode) + .filter(Objects::nonNull) + .collect(Collectors.toSet()), + (a, b) -> a)); + + StringBuilder failureMsg = new StringBuilder(); + + // 2. 阶段一:逐行业简单计数校验(行业内本身人员是否充足),不通过直接返回 + for (Map.Entry> entry : standardsGroupByIndustryMap.entrySet()) { + appendPhaseOneFailures(entry.getKey(), entry.getValue(), personCapabilityMap, failureMsg); + } + if (failureMsg.length() > 0) { + return Pair.of(false, failureMsg.toString()); + } + + // 3. 阶段二:全局最优分配校验(一人一专业,多业务范围不重复计算) + // 构建需求(复合键 industryCode::capCode)和 能力→需求键 映射 + Map demandMap = new LinkedHashMap<>(); + Map> capToDemandKeys = new HashMap<>(); + for (Map.Entry> entry : standardsGroupByIndustryMap.entrySet()) { + String industryCode = entry.getKey(); + for (IndustryProfessionalStandardsDO standard : entry.getValue()) { + String capCode = standard.getProfessionalCapabilityCode(); + String demandKey = industryCode + "::" + capCode; + demandMap.put(demandKey, standard.getPersonNum() == null ? 0L : standard.getPersonNum()); + capToDemandKeys.computeIfAbsent(capCode, k -> new ArrayList<>()).add(demandKey); } } - return failureMsg.length() > 1 ? Pair.of(false, failureMsg.toString()) : Pair.of(true, null); + + // 构建人员 → 需求键集合 + Map> personDemandMap = new HashMap<>(); + for (Map.Entry> entry : personCapabilityMap.entrySet()) { + Set demandKeys = new HashSet<>(); + for (String cap : entry.getValue()) { + List keys = capToDemandKeys.get(cap); + if (keys != null) { + demandKeys.addAll(keys); + } + } + if (!demandKeys.isEmpty()) { + personDemandMap.put(entry.getKey(), demandKeys); + } + } + Map assignedMap = canFullyAssign(personDemandMap, demandMap); + + // 比对分配结果,生成失败明细 + for (Map.Entry> entry : standardsGroupByIndustryMap.entrySet()) { + String industryCode = entry.getKey(); + for (IndustryProfessionalStandardsDO standard : entry.getValue()) { + String capCode = standard.getProfessionalCapabilityCode(); + long required = standard.getPersonNum() == null ? 0L : standard.getPersonNum(); + long assigned = assignedMap.getOrDefault(industryCode + "::" + capCode, 0L); + appendIfShort(industryCode, capCode, required, assigned, failureMsg); + } + } + + return failureMsg.length() > 0 ? Pair.of(false, failureMsg.toString()) : Pair.of(true, null); + } + + /** + * 阶段一:统计某项专业能力的可用人数(具备该能力的人员总数),不足则追加失败明细 + */ + private static void appendPhaseOneFailures(String industryCode, List standards + , Map> personCapabilityMap, StringBuilder failureMsg) { + String industryName = IndustryEnum.getValueByCode(industryCode); + for (IndustryProfessionalStandardsDO standard : standards) { + String capCode = standard.getProfessionalCapabilityCode(); + long required = standard.getPersonNum() == null ? 0L : standard.getPersonNum(); + long available = personCapabilityMap.values().stream() + .filter(caps -> caps.contains(capCode)) + .count(); + appendIfShort(industryName, capCode, required, available, failureMsg); + } + } + + /** + * 当实际人数小于需求时,追加一条失败明细 + */ + private static void appendIfShort(String industryOrCode, String capCode, long required + , long available, StringBuilder failureMsg) { + if (available >= required) { + return; + } + IndustryEnum industryEnum = IndustryEnum.ofCode(industryOrCode); + String industryName = industryEnum != null ? industryEnum.getValue() : industryOrCode; + ProfessionalCapabilityEnum capEnum = ProfessionalCapabilityEnum.ofCode(capCode); + String capName = capEnum != null ? capEnum.getValue() : capCode; + failureMsg.append(industryName) + .append(CommonUtil.COLON) + .append(capName) + .append(" 需要") + .append(required) + .append("人,当前可用") + .append(available) + .append("人;"); } /** diff --git a/safety-eval-infrastructure/src/main/resources/mapper/OrgPersonnelCertMapper.xml b/safety-eval-infrastructure/src/main/resources/mapper/OrgPersonnelCertMapper.xml index aa869bfd..0281429f 100644 --- a/safety-eval-infrastructure/src/main/resources/mapper/OrgPersonnelCertMapper.xml +++ b/safety-eval-infrastructure/src/main/resources/mapper/OrgPersonnelCertMapper.xml @@ -60,6 +60,22 @@ + + +