weaver-hrm-salary/src/com/engine/salary/entity/salaryacct/bo/SalaryCalcItemGraphTemp.java

196 lines
9.4 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/**
* 对薪资核算时涉及的薪资项目进行排序
* <p>Copyright: Copyright (c) 2023</p>
* <p>Company: 泛微软件</p>
*
* @author qiantao
* @version 1.0
**/
public class SalaryCalcItemGraphTemp {
private List<SalaryCalcItemGraphNode> nodes;
private Map<Long, String> items;
/**
* 根据薪资账套的薪资项目、公式详情构建实例
*
* @param salarySobItems 薪资账套的薪资项目
* @param expressFormulas 公式详情
*/
public SalaryCalcItemGraphTemp(List<SalarySobItemPO> salarySobItems, List<SalaryItemPO> salaryItemPOS, List<ExpressFormula> expressFormulas) {
Map<String, SalaryItemPO> salaryItemMap = SalaryEntityUtil.convert2Map(salaryItemPOS, SalaryItemPO::getCode);
Map<Long, ExpressFormula> expressFormulaMap = SalaryEntityUtil.convert2Map(expressFormulas, ExpressFormula::getId);
Map<Long, List<FormulaVar>> formulaVarMap = ExpressFormulaBO.buildFormulaVar(expressFormulas);
// key薪资项目的codevalue薪资项目的po
Map<Long, SalaryItemPO> codeKeySalaryItemPOMap = SalaryEntityUtil.convert2Map(salaryItemPOS, SalaryItemPO::getId);
salarySobItems.forEach(salarySobItem -> salarySobItem.setSalaryItemCode(codeKeySalaryItemPOMap.get(salarySobItem.getSalaryItemId()).getCode()));
Map<String, SalarySobItemPO> salarySobItemMap = SalaryEntityUtil.convert2Map(salarySobItems, SalarySobItemPO::getSalaryItemCode);
Map<Long, SalaryCalcItemGraphNode> 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<FormulaVar> 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<SalaryCalcItem> sort() {
// 循环遍历所有顶点
for (SalaryCalcItemGraphNode node : nodes) {
// 循环遍历每个顶点的邻居
for (SalaryCalcItemGraphNode destNode : node.getDestNodes()) {
// 维护每个顶点的入度
destNode.setInDegree(destNode.getInDegree() + 1);
}
}
// 创建一个队列
LinkedTransferQueue<SalaryCalcItemGraphNode> queue = new LinkedTransferQueue<>();
// 再次循环所有顶点
for (SalaryCalcItemGraphNode node : nodes) {
// 若该顶点入度为0则加入队列
if (node.getInDegree() == 0) {
queue.offer(node);
}
}
// 存储结果的列表
List<SalaryCalcItem> 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<Long> 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<SalaryCalcItemGraphNode> 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<>();
}
}
}