2024-11-26 11:44:51 +08:00
|
|
|
|
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
|
|
|
|
|
|
**/
|
2024-11-26 11:45:48 +08:00
|
|
|
|
public class SalaryCalcItemGraphTemp {
|
2024-11-26 11:44:51 +08:00
|
|
|
|
|
|
|
|
|
|
private List<SalaryCalcItemGraphNode> nodes;
|
|
|
|
|
|
private Map<Long, String> items;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据薪资账套的薪资项目、公式详情构建实例
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param salarySobItems 薪资账套的薪资项目
|
|
|
|
|
|
* @param expressFormulas 公式详情
|
|
|
|
|
|
*/
|
2024-11-26 11:45:48 +08:00
|
|
|
|
public SalaryCalcItemGraphTemp(List<SalarySobItemPO> salarySobItems, List<SalaryItemPO> salaryItemPOS, List<ExpressFormula> expressFormulas) {
|
2024-11-26 11:44:51 +08:00
|
|
|
|
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:薪资项目的code,value:薪资项目的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<>();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|