package com.engine.salary.entity.salaryacct.bo; import com.engine.salary.entity.salaryformula.ExpressFormula; import com.engine.salary.entity.salaryformula.po.FormulaVar; import com.engine.salary.entity.salaryitem.po.SalaryItemPO; import com.engine.salary.entity.salarysob.po.SalarySobItemPO; import com.engine.salary.enums.salaryformula.SalaryFormulaReferenceEnum; import com.engine.salary.exception.SalaryRunTimeException; import com.engine.salary.report.common.constant.SalaryConstant; import com.engine.salary.util.SalaryEntityUtil; import com.google.common.collect.Lists; import lombok.Data; import org.apache.commons.lang3.StringUtils; import java.util.*; import java.util.concurrent.LinkedTransferQueue; import java.util.stream.Collectors; /** * 对薪资核算时涉及的薪资项目进行排序 *

Copyright: Copyright (c) 2023

*

Company: 泛微软件

* * @author qiantao * @version 1.0 **/ public class SalaryCalcItemGraphTemp { private List nodes; private Map items; /** * 根据薪资账套的薪资项目、公式详情构建实例 * * @param salarySobItems 薪资账套的薪资项目 * @param expressFormulas 公式详情 */ public SalaryCalcItemGraphTemp(List salarySobItems, List salaryItemPOS, List expressFormulas) { Map salaryItemMap = SalaryEntityUtil.convert2Map(salaryItemPOS, SalaryItemPO::getCode); Map expressFormulaMap = SalaryEntityUtil.convert2Map(expressFormulas, ExpressFormula::getId); Map> formulaVarMap = ExpressFormulaBO.buildFormulaVar(expressFormulas); // key:薪资项目的code,value:薪资项目的po Map codeKeySalaryItemPOMap = SalaryEntityUtil.convert2Map(salaryItemPOS, SalaryItemPO::getId); salarySobItems.forEach(salarySobItem -> salarySobItem.setSalaryItemCode(codeKeySalaryItemPOMap.get(salarySobItem.getSalaryItemId()).getCode())); Map salarySobItemMap = SalaryEntityUtil.convert2Map(salarySobItems, SalarySobItemPO::getSalaryItemCode); Map nodeMap = new HashMap<>(); for (SalarySobItemPO salarySobItem : salarySobItems) { ExpressFormula expressFormula = expressFormulaMap.get(salarySobItem.getFormulaId()); SalaryCalcItemGraphNode node = nodeMap.computeIfAbsent(salarySobItem.getSalaryItemId(), key -> new SalaryCalcItemGraphNode(salarySobItem, expressFormula)); List formulaVars = expressFormula == null ? Collections.emptyList() : formulaVarMap.getOrDefault(expressFormula.getId(), Collections.emptyList()); for (FormulaVar formulaVar : formulaVars) { if (StringUtils.isNotEmpty(formulaVar.getFieldId()) && StringUtils.startsWith(formulaVar.getFieldId(), SalaryFormulaReferenceEnum.SALARY_ITEM.getValue() + SalaryConstant.FORMULA_VAR_SEPARATOR)) { String salaryItemCode = formulaVar.getFieldId().split(SalaryConstant.FORMULA_VAR_SEPARATOR)[1]; SalarySobItemPO subSalarySobItem = salarySobItemMap.get(salaryItemCode); if (subSalarySobItem == null) { SalaryItemPO salaryItemPO = salaryItemMap.get(salaryItemCode); if (salaryItemPO == null) { continue; } else { subSalarySobItem = SalarySobItemPO.builder().salaryItemId(salaryItemPO.getId()).salaryItemCode(salaryItemPO.getCode()).build(); } } ExpressFormula subExpressFormula = expressFormulaMap.get(subSalarySobItem.getFormulaId()); SalarySobItemPO finalSubSalarySobItem = subSalarySobItem; SalaryCalcItemGraphNode destNode = nodeMap.computeIfAbsent(subSalarySobItem.getSalaryItemId(), key -> new SalaryCalcItemGraphNode(finalSubSalarySobItem, subExpressFormula)); node.getDestNodes().add(destNode); } } } this.nodes = Lists.newArrayList(nodeMap.values()); this.items = SalaryEntityUtil.convert2Map(salaryItemPOS, SalaryItemPO::getId, SalaryItemPO::getName); } /** * 对薪资核算时涉及的薪资项目进行排序 * 对于一次薪资核算而言,不同的薪资项目之间可能会存在引用。 * 例如「绩效工资=绩效比例*(基本工资+岗位工资)」,在计算「绩效工资」前需要先计算出「绩效比例」、「基本工资」、「岗位工资」 * 所以薪资核算前需要对所涉及的所有薪资项目进行一个计算排序 * 对薪资项目的排序就相当于是对一个「有向无环的图」进行拓扑排序,一个薪资项目就相当于图中的一个「顶点」 * 具体算法如下: * 1、根据「薪资账套的薪资项目」、「公式详情」构建出所有的「顶点」 * 2、将所有顶点的入度都初始化为0 * 3、遍历所有的顶点,计算每个顶点的入度 * 4、创建一个队列,将所有入度为0的顶点都存入队列 * 5、每次从队列中取出一个点,将该点存入结果集合中,并将该顶点的所有邻接点的入度减1。如果邻接点的入度变为了0,将其加入队列中 * 6、重复步骤5,直到队列空了 * * @return 排好序的薪资项目id */ public List sort() { // 循环遍历所有顶点 for (SalaryCalcItemGraphNode node : nodes) { // 循环遍历每个顶点的邻居 for (SalaryCalcItemGraphNode destNode : node.getDestNodes()) { // 维护每个顶点的入度 destNode.setInDegree(destNode.getInDegree() + 1); } } // 创建一个队列 LinkedTransferQueue queue = new LinkedTransferQueue<>(); // 再次循环所有顶点 for (SalaryCalcItemGraphNode node : nodes) { // 若该顶点入度为0,则加入队列 if (node.getInDegree() == 0) { queue.offer(node); } } // 存储结果的列表 List result = new ArrayList<>(); // 循环直到队列为空 while (!queue.isEmpty()) { // 取出队列头部的顶点编号 SalaryCalcItemGraphNode node = queue.poll(); // 将该顶点加入到结果列表中 result.add(node.getSalaryCalcItem()); // 标记该顶点已被访问 node.setVisited(true); // 遍历该顶点的邻居 for (SalaryCalcItemGraphNode destNode : node.getDestNodes()) { // 若该邻居未被访问过 if (!destNode.isVisited()) { // 将该邻居的入度减一 destNode.setInDegree(destNode.getInDegree() - 1); // 若该邻居入度已经变为0 if (destNode.getInDegree() == 0) { // 将该邻居加入到队列中 queue.offer(destNode); } } } } // 倒序 Collections.reverse(result); if (!Objects.equals(result.size(), nodes.size())) { List resultIds = SalaryEntityUtil.properties(result, SalaryCalcItem::getSalaryItemId, Collectors.toList()); String errItemName = nodes.stream() .map(SalaryCalcItemGraphNode::getSalaryCalcItem) .map(SalaryCalcItem::getSalaryItemId) .filter(itemId -> !resultIds.contains(itemId)) .map(itemId -> items.getOrDefault(itemId, "")) .collect((Collectors.joining(",", "[", "]"))); throw new SalaryRunTimeException("薪资项目:" + errItemName + "存在闭环!"); } // 返回结果列表 return result; } @Data private static class SalaryCalcItemGraphNode { /** * 薪资项目的id */ private SalaryCalcItem salaryCalcItem; /** * 入度 */ private int inDegree; /** * 是否已被访问 */ private boolean visited; /** * 当前顶点的邻居 */ private List destNodes; public SalaryCalcItemGraphNode(SalarySobItemPO salarySobItem, ExpressFormula expressFormula) { SalaryCalcItem salaryCalcItem = new SalaryCalcItem(); salaryCalcItem.setSalaryItemId(salarySobItem.getSalaryItemId()); salaryCalcItem.setSalaryItemCode(salarySobItem.getSalaryItemCode()); // salaryCalcItem.setIncomeCategory(salarySobItem.getIncomeCategory()); // salaryCalcItem.setUseInEmployeeSalary(salarySobItem.getUseInEmployeeSalary()); // salaryCalcItem.setDataType(salarySobItem.getDataType()); salaryCalcItem.setRoundingMode(salarySobItem.getRoundingMode()); salaryCalcItem.setPattern(salarySobItem.getPattern()); salaryCalcItem.setExpressFormula(expressFormula); this.salaryCalcItem = salaryCalcItem; this.inDegree = 0; this.visited = false; this.destNodes = new ArrayList<>(); } } }