upgrade
parent
f0baa891af
commit
d35e46046f
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<CapabilityAssessmentDTO> capabilityAssessment;
|
||||
|
||||
|
||||
@ApiModelProperty(value = "是否技术负责人(false否true是)")
|
||||
private Boolean technicalDirectorFlag;
|
||||
|
||||
|
|
@ -33,6 +34,6 @@ public class QualFilingPersonnelFilingCheckDTO {
|
|||
@ApiModelProperty(value = "职称(多个逗号分隔)")
|
||||
private List<TitleCodeCo> titleCodeArr;
|
||||
|
||||
// @ApiModelProperty(value = "人员证书信息")
|
||||
// private List<PersonnelCertFilingPageCO> personnelCertFilingPageCOList;
|
||||
@ApiModelProperty(value = "人员证书信息")
|
||||
private List<PersonnelCertFilingCheckCO> personnelCertFilingCheckCOList;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<OrgPersonnelCertDO> {
|
|||
List<OrgPersonnelCertCapabilityComplianceCheckCO> selectCapabilityByPersonnelIds(
|
||||
@Param("personnelIds") List<Long> personnelIds);
|
||||
|
||||
/**
|
||||
* 根据人员id列表查询人员证书信息
|
||||
*
|
||||
* @param personnelIds 人员id列表
|
||||
* @return 人员证书列表
|
||||
*/
|
||||
List<PersonnelCertFilingCheckCO> selectCertByPersonnelIds(@Param("personnelIds") List<Long> personnelIds);
|
||||
|
||||
/**
|
||||
* 根据主键ID查询人员证书详情(关联industry_professional_standards获取专业能力名称)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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<QualFilingPersonnelFilingCheckDTO> result) {
|
||||
List<Long> personnelIds = result.stream()
|
||||
.map(QualFilingPersonnelFilingCheckDTO::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<PersonnelCertFilingCheckCO> certList = SpringUtil.getBean(OrgPersonnelCertMapper.class)
|
||||
.selectCertByPersonnelIds(personnelIds);
|
||||
Map<Long, List<PersonnelCertFilingCheckCO>> certMap = certList.stream()
|
||||
.collect(Collectors.groupingBy(PersonnelCertFilingCheckCO::getPersonnelId));
|
||||
for (QualFilingPersonnelFilingCheckDTO dto : result) {
|
||||
dto.setPersonnelCertFilingCheckCOList(certMap.getOrDefault(dto.getId(), Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将人员备案检查数据中的职称信息回填到备案人员列表。
|
||||
*
|
||||
|
|
@ -277,16 +302,22 @@ public class ComplianceCheckUtil {
|
|||
QualFilingComplianceCheckEnum.STATUS_FAIL, "无人员数据");
|
||||
}
|
||||
int total = personnelList.size();
|
||||
String code = CertTypeCodeEnum.REGISTERED_SAFETY_ENGINEER.getCode();
|
||||
|
||||
// 中/高 级注册安全工程师
|
||||
Set<Long> 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<Long> 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);
|
||||
|
|
@ -299,19 +330,9 @@ public class ComplianceCheckUtil {
|
|||
throw new BizException(PERSON_NOT_ENOUGH, currentValue);
|
||||
}
|
||||
|
||||
// 根据人员id查询专业能力code并设置到CO中
|
||||
List<Long> sourcePersonnelIds = personnelList.stream()
|
||||
.map(QualFilingPersonnelAddCmd::getSourcePersonnelId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
List<IndustryProfessionalPersonCo> personnelCoList = SpringUtil.getBean(OrgPersonnelMapper.class)
|
||||
.selectProfessionalCapabilityByPersonnelIds(sourcePersonnelIds);
|
||||
Pair<Boolean, String> booleanStringPair = industryProfessionalStandardsValid(
|
||||
businessScope, personnelCoList, null);
|
||||
|
||||
/*Pair<Boolean, String> booleanStringPair = industryProfessionalStandardsValid(
|
||||
businessScope, personnelCoList, personnelCertCoList);*/
|
||||
|
||||
// 安全评价机构业务范围与专业能力配备标准 校验
|
||||
Pair<Boolean, String> booleanStringPair =
|
||||
industryProfessionalStandardsPersonValid(businessScope, personnelCertCoList);
|
||||
boolean personPassed = Boolean.TRUE.equals(booleanStringPair.getKey());
|
||||
if (!personPassed && throwException) {
|
||||
throw new BizException(PERSON_NOT_ENOUGH, booleanStringPair.getValue());
|
||||
|
|
@ -439,19 +460,15 @@ public class ComplianceCheckUtil {
|
|||
}
|
||||
|
||||
/**
|
||||
* industryProfessionalStandardsValid
|
||||
* 安全评价机构业务范围与专业能力配备标准 校验
|
||||
*
|
||||
* @param industryCodeList industryCodeList
|
||||
* @param personnelCoList industryProfessionalPersonCos
|
||||
* @param personnelCertCoList certCapabilityList
|
||||
* @return Pair<Boolean, String> key 是否通过 ,value 失败消息(通过时为 null)
|
||||
*/
|
||||
public static Pair<Boolean, String> industryProfessionalStandardsValid(List<String> industryCodeList
|
||||
, List<IndustryProfessionalPersonCo> personnelCoList
|
||||
, List<OrgPersonnelCertCapabilityComplianceCheckCO> personnelCertCoList) {
|
||||
if (CollectionUtils.isEmpty(industryCodeList)) {
|
||||
return Pair.of(false, "没有业务范围");
|
||||
}
|
||||
public static Pair<Boolean, String> industryProfessionalStandardsPersonValid(List<String> industryCodeList
|
||||
, List<QualFilingPersonnelFilingCheckDTO> personnelCertCoList) {
|
||||
|
||||
// 通过 SpringUtil 获取 Mapper,根据 industryCodeList 查询专业能力配备标准
|
||||
IndustryProfessionalStandardsMapper mapper = SpringUtil.getBean(IndustryProfessionalStandardsMapper.class);
|
||||
LambdaQueryWrapper<IndustryProfessionalStandardsDO> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
|
@ -460,29 +477,145 @@ public class ComplianceCheckUtil {
|
|||
List<IndustryProfessionalStandardsDO> standardsDOList = mapper.selectList(queryWrapper);
|
||||
|
||||
if (CollectionUtils.isEmpty(standardsDOList)) {
|
||||
log.warn("没有配置标准");
|
||||
return Pair.of(true, null);
|
||||
log.warn("务范围与专业能力配备标准未开启");
|
||||
return Pair.of(false, "务范围与专业能力配备标准未开启");
|
||||
}
|
||||
|
||||
// 按 industryCode 分组后循环调用 doIndustryProfessionalStandardsValid
|
||||
Map<String, List<IndustryProfessionalStandardsDO>> 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<IndustryProfessionalPersonCo> personnelCoCopyList = new ArrayList<>(personnelCoList);
|
||||
|
||||
Map<String, List<OrgPersonnelCertCapabilityComplianceCheckCO>> personnelCertCoGroupByIndustry =
|
||||
personnelCertCoList.stream().collect(
|
||||
Collectors.groupingBy(OrgPersonnelCertCapabilityComplianceCheckCO::getBusinessScope));
|
||||
return doIndustryProfessionalStandardsValid(standardsGroupByIndustry, personnelCertCoList);
|
||||
}
|
||||
|
||||
StringJoiner failureMsg = new StringJoiner(";");
|
||||
for (Map.Entry<String, List<IndustryProfessionalStandardsDO>> entry : standardsGroupByIndustry.entrySet()) {
|
||||
Pair<Boolean, String> result = doIndustryProfessionalStandardsValid(entry.getValue(), personnelCoCopyList
|
||||
, personnelCertCoGroupByIndustry.getOrDefault(entry.getKey(), new ArrayList<>()));
|
||||
if (Boolean.FALSE.equals(result.getKey()) && result.getValue() != null) {
|
||||
failureMsg.add(result.getValue());
|
||||
|
||||
/**
|
||||
* 安全评价机构业务范围与专业能力配备标准 校验
|
||||
* <p>
|
||||
* 核心约束:一人只能代表一个专业,申请多个业务范围时人员不能重复计算。
|
||||
* 分两阶段校验:
|
||||
* <ul>
|
||||
* <li>阶段一(逐行业计数):逐个行业检查所需要的能力人员是否充足,不足即记录错误并提前返回。</li>
|
||||
* <li>阶段二(全局最优分配):所有行业都通过后,使用最大流算法(Dinic)在
|
||||
* "一人一专业、多业务范围不重复计算" 的约束下,判断是否存在全局分配方案满足所有需求。</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param standardsGroupByIndustryMap 是标准的行业要求 枚举类 IndustryEnum
|
||||
* @param personnelCertCoList 人员的能力在 capabilityAssessment.professionalCapabilityCode
|
||||
* @return Pair<Boolean, String> key 是否通过 ,value 失败消息(通过时为 null)
|
||||
*/
|
||||
public static Pair<Boolean, String> doIndustryProfessionalStandardsValid(
|
||||
Map<String, List<IndustryProfessionalStandardsDO>> standardsGroupByIndustryMap
|
||||
, List<QualFilingPersonnelFilingCheckDTO> personnelCertCoList) {
|
||||
|
||||
// 1. 收集人员 → 具备的专业能力code集合(来源于能力认定)
|
||||
Map<Long, Set<String>> 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<String, List<IndustryProfessionalStandardsDO>> entry : standardsGroupByIndustryMap.entrySet()) {
|
||||
appendPhaseOneFailures(entry.getKey(), entry.getValue(), personCapabilityMap, failureMsg);
|
||||
}
|
||||
if (failureMsg.length() > 0) {
|
||||
return Pair.of(false, failureMsg.toString());
|
||||
}
|
||||
|
||||
// 3. 阶段二:全局最优分配校验(一人一专业,多业务范围不重复计算)
|
||||
// 构建需求(复合键 industryCode::capCode)和 能力→需求键 映射
|
||||
Map<String, Long> demandMap = new LinkedHashMap<>();
|
||||
Map<String, List<String>> capToDemandKeys = new HashMap<>();
|
||||
for (Map.Entry<String, List<IndustryProfessionalStandardsDO>> 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<Long, Set<String>> personDemandMap = new HashMap<>();
|
||||
for (Map.Entry<Long, Set<String>> entry : personCapabilityMap.entrySet()) {
|
||||
Set<String> demandKeys = new HashSet<>();
|
||||
for (String cap : entry.getValue()) {
|
||||
List<String> keys = capToDemandKeys.get(cap);
|
||||
if (keys != null) {
|
||||
demandKeys.addAll(keys);
|
||||
}
|
||||
}
|
||||
if (!demandKeys.isEmpty()) {
|
||||
personDemandMap.put(entry.getKey(), demandKeys);
|
||||
}
|
||||
}
|
||||
Map<String, Long> assignedMap = canFullyAssign(personDemandMap, demandMap);
|
||||
|
||||
// 比对分配结果,生成失败明细
|
||||
for (Map.Entry<String, List<IndustryProfessionalStandardsDO>> 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<IndustryProfessionalStandardsDO> standards
|
||||
, Map<Long, Set<String>> 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("人;");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -60,6 +60,22 @@
|
|||
</foreach>
|
||||
</select>
|
||||
|
||||
<!-- 根据人员id列表查询人员证书信息 -->
|
||||
<select id="selectCertByPersonnelIds"
|
||||
resultType="org.qinan.safetyeval.client.co.PersonnelCertFilingCheckCO">
|
||||
SELECT opc.id,
|
||||
opc.personnel_id,
|
||||
opc.professional_capability_code,
|
||||
opc.cert_type_code,
|
||||
opc.cert_level
|
||||
FROM org_personnel_cert opc
|
||||
WHERE opc.delete_enum = 'false' and opc.personnel_id IN
|
||||
<foreach collection="personnelIds" item="personnelId" open="(" separator="," close=")">
|
||||
#{personnelId}
|
||||
</foreach>
|
||||
|
||||
</select>
|
||||
|
||||
<!-- 根据主键ID查询人员证书详情(关联industry_professional_standards获取专业能力名称) -->
|
||||
<select id="selectDetailById"
|
||||
resultType="org.qinan.safetyeval.client.co.OrgPersonnelCertDetailCO">
|
||||
|
|
|
|||
Loading…
Reference in New Issue