This commit is contained in:
2024-08-07 22:24:53 +08:00
parent a67b9af49d
commit 3ea4a268d1
4 changed files with 377 additions and 53 deletions

View File

@@ -21,9 +21,16 @@ import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.builder.AstStringCompiler;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.SecureASTCustomizer;
import org.springframework.util.StopWatch;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -40,6 +47,34 @@ public class GroovyScriptUtils {
private static final Cache<Class<?>, Script> scriptInstanceCache =
CacheBuilder.newBuilder().maximumSize(10000).build();
public static SecureASTCustomizer SecurityCustomizer =
new SecureASTCustomizer() {
{
setAllowedImports(List.of("java.lang.Math"));
setAllowedConstantTypesClasses(
Arrays.asList(
Integer.class,
Long.class,
Double.class,
Float.class,
String.class,
Boolean.class,
LocalDateTime.class,
LocalDate.class,
LocalTime.class,
YearMonth.class,
Math.class));
setAllowedReceiversClasses(
Arrays.asList(
Math.class,
Integer.class,
Float.class,
Double.class,
Long.class,
BigDecimal.class));
}
};
public String replaceVariable(
String patternstr, String script, Function<String, String> mapper) {
@@ -75,6 +110,7 @@ public class GroovyScriptUtils {
throw new BizException("无效的计算公式: " + e.getMessage());
}
}
private static final GroovyClassLoader groovyClassLoader;
static {
@@ -110,11 +146,15 @@ public class GroovyScriptUtils {
@SneakyThrows
public static Object exec(String formula0, Map<String, Object> map) {
return exec(formula0, map, true, false);
return exec(formula0, map, true, false);
}
@SneakyThrows
public static Object exec(String formula0, Map<String, Object> map, boolean reuseScriptInstance, boolean logTime) {
public static Object exec(
String formula0,
Map<String, Object> map,
boolean reuseScriptInstance,
boolean logTime) {
if (StringUtils.isEmpty(formula0)) {
return null;
@@ -140,8 +180,11 @@ public class GroovyScriptUtils {
if (stopWatch != null) stopWatch.start("newInstance");
Script script = reuseScriptInstance ? scriptInstanceCache.get(parsedClass, () -> (Script) parsedClass.newInstance()): (Script) parsedClass.newInstance();
Script script =
reuseScriptInstance
? scriptInstanceCache.get(
parsedClass, () -> (Script) parsedClass.newInstance())
: (Script) parsedClass.newInstance();
if (stopWatch != null) stopWatch.stop();

View File

@@ -2,6 +2,7 @@ package cn.lihongjie.coal.empSalary.entity;
import cn.lihongjie.coal.base.entity.OrgCommonEntity;
import cn.lihongjie.coal.common.DictCode;
import cn.lihongjie.coal.empMonthAttendance.entity.EmpMonthAttendanceEntity;
import cn.lihongjie.coal.empSalaryBatch.entity.EmpSalaryBatchEntity;
import cn.lihongjie.coal.employee.entity.EmployeeEntity;
import cn.lihongjie.coal.pojoProcessor.DictTranslate;
@@ -28,6 +29,9 @@ public class EmpSalaryEntity extends OrgCommonEntity {
private EmployeeEntity employee;
@ManyToOne
private EmpMonthAttendanceEntity empMonthAttendance;
private BigDecimal item0;
private BigDecimal item1;
@@ -172,7 +176,9 @@ public class EmpSalaryEntity extends OrgCommonEntity {
* 员工信息冗余字段
*/
private String empName;
private String empCode;
@Comment("性别")
private String sex;

View File

@@ -2,17 +2,36 @@ package cn.lihongjie.coal.empSalary.service;
import cn.lihongjie.coal.base.dto.CommonQuery;
import cn.lihongjie.coal.base.dto.IdRequest;
import cn.lihongjie.coal.base.entity.OrgCommonEntity;
import cn.lihongjie.coal.base.service.BaseService;
import cn.lihongjie.coal.common.GroovyScriptUtils;
import cn.lihongjie.coal.common.ReflectUtils;
import cn.lihongjie.coal.empMonthAttendance.entity.EmpMonthAttendanceEntity;
import cn.lihongjie.coal.empSalary.dto.CreateEmpSalaryDto;
import cn.lihongjie.coal.empSalary.dto.EmpSalaryDto;
import cn.lihongjie.coal.empSalary.dto.UpdateEmpSalaryDto;
import cn.lihongjie.coal.empSalary.entity.EmpSalaryEntity;
import cn.lihongjie.coal.empSalary.mapper.EmpSalaryMapper;
import cn.lihongjie.coal.empSalary.repository.EmpSalaryRepository;
import cn.lihongjie.coal.empSalaryBatch.entity.EmpSalaryBatchEntity;
import cn.lihongjie.coal.empSalaryItem.service.EmpSalaryItemService;
import cn.lihongjie.coal.employee.entity.EmployeeEntity;
import cn.lihongjie.coal.exception.BizException;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.Script;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.jetbrains.annotations.NotNull;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Page;
@@ -20,6 +39,13 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StopWatch;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@Slf4j
@@ -38,24 +64,8 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
return getById(entity.getId());
}
public EmpSalaryDto update(UpdateEmpSalaryDto request) {
EmpSalaryEntity entity = this.repository.get(request.getId());
if (this.repository.containArchived(request.getId())){
throw new BizException("部分数据已归档,无法编辑或删除");
}
this.mapper.updateEntity(entity, request);
this.repository.save(entity);
return getById(entity.getId());
}
public void delete(IdRequest request) {
if (this.repository.containArchived(request)){
throw new BizException("部分数据已归档,无法编辑或删除");
}
this.repository.deleteAllById(request.getIds());
}
@PersistenceContext EntityManager em;
@Autowired RedissonClient redissonClient;
public EmpSalaryDto getById(String id) {
EmpSalaryEntity entity = repository.get(id);
@@ -82,4 +92,246 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
public void unarchive(IdRequest dto) {
this.repository.unArchive(dto);
}
@Autowired EmpSalaryItemService empSalaryItemService;
public EmpSalaryDto update(UpdateEmpSalaryDto request) {
EmpSalaryEntity entity = this.repository.get(request.getId());
if (this.repository.containArchived(request.getId())) {
throw new BizException("部分数据已归档,无法编辑或删除");
}
this.mapper.updateEntity(entity, request);
this.repository.save(entity);
return getById(entity.getId());
}
public void delete(IdRequest request) {
if (this.repository.containArchived(request)) {
throw new BizException("部分数据已归档,无法编辑或删除");
}
this.repository.deleteAllById(request.getIds());
}
/**
* 初始化工资
*
* @param batch
* @param employees
*/
@SneakyThrows
public void initSalary(EmpSalaryBatchEntity batch, List<EmployeeEntity> employees) {
StopWatch stopWatch = new StopWatch("initSalary: " + batch.getId());
stopWatch.start("getLock");
RLock lock = redissonClient.getLock("initSalary." + batch.getId());
boolean tryLock = lock.tryLock();
stopWatch.stop();
if (!tryLock) {
throw new BizException(
String.format(
"批次 %s-%s-%s 正在初始化中",
batch.getBatchYearMonth().getYear(),
batch.getBatchYearMonth().getMonthValue(),
batch.getBatchNo()));
}
GroovyClassLoader groovyClassLoader = null;
try {
// 判断批次状态
stopWatch.start("checkBatchStatus");
checkBatchStatus(batch);
stopWatch.stop();
// 判断已有工资数据是否重复
stopWatch.start("checkDuplicate");
checkDuplicate(batch, employees);
stopWatch.stop();
// 查询考勤数据
stopWatch.start("queryAttendanceMap");
Map<String, List<EmpMonthAttendanceEntity>> attMap =
queryAttendanceMap(batch, employees);
stopWatch.stop();
stopWatch.start("genScript");
// 生成计算脚本
String script = "";
try {
script = empSalaryItemService.genScript(batch.getOrganizationId());
} catch (Exception e) {
log.error("生成计算脚本失败", e);
throw new BizException("生成计算脚本失败");
}
stopWatch.stop();
stopWatch.start("parseScript");
CompilerConfiguration config = new CompilerConfiguration();
config.addCompilationCustomizers(GroovyScriptUtils.SecurityCustomizer);
groovyClassLoader =
new GroovyClassLoader(GroovyScriptUtils.class.getClassLoader(), config);
Class parsedClass = groovyClassLoader.parseClass(script);
Script scriptObj = (Script) parsedClass.newInstance();
stopWatch.stop();
// 执行计算脚本
List<EmpSalaryEntity> salaries = new ArrayList<>();
for (EmployeeEntity employee : employees) {
EmpMonthAttendanceEntity attendance =
attMap.containsKey(employee.getId())
? attMap.get(employee.getId()).get(0)
: null;
stopWatch.start("buildCtx: " + employee.getName());
Map<String, Object> ctx = buildCtx(batch, employee, attendance, null);
stopWatch.stop();
// todo 处理继承项
// 计算
stopWatch.start("execScript: " + employee.getName());
scriptObj.setBinding(new Binding(Map.of("salary", ctx)));
scriptObj.run();
stopWatch.stop();
// 转化为工资数据
stopWatch.start("convertToSalary: " + employee.getName());
EmpSalaryEntity salaryEntity = ReflectUtils.fromMap(ctx, EmpSalaryEntity.class);
salaryEntity.setBatch(batch);
salaryEntity.setEmployee(employee);
salaryEntity.setEmpMonthAttendance(attendance);
stopWatch.stop();
salaries.add(salaryEntity);
}
// 保存到数据库
stopWatch.start("saveAll");
this.repository.saveAll(salaries);
stopWatch.stop();
} finally {
lock.unlock();
if (stopWatch.isRunning()) {
stopWatch.stop();
}
if (groovyClassLoader != null) {
groovyClassLoader.close();
}
log.info(stopWatch.prettyPrint());
}
}
private @NotNull Map<String, List<EmpMonthAttendanceEntity>> queryAttendanceMap(
EmpSalaryBatchEntity batch, List<EmployeeEntity> employees) {
List<EmpMonthAttendanceEntity> attendances =
em.createQuery(
"select a from EmpMonthAttendanceEntity a where a.employee.id in :empIds and a.yearMonth = :yearMonth",
EmpMonthAttendanceEntity.class)
.setParameter(
"empIds", employees.stream().map(EmployeeEntity::getId).toList())
.setParameter("yearMonth", batch.getBatchYearMonth())
.getResultList();
Map<String, List<EmpMonthAttendanceEntity>> attMap =
attendances.stream().collect(Collectors.groupingBy(e -> e.getEmployee().getId()));
return attMap;
}
private void checkBatchStatus(EmpSalaryBatchEntity batch) {}
private void checkDuplicate(EmpSalaryBatchEntity batch, List<EmployeeEntity> employees) {
List<EmpSalaryEntity> exists =
em.createQuery(
"select s from EmpSalaryEntity s where s.batch.id = :batchId and s.employee.id in :empIds",
EmpSalaryEntity.class)
.setParameter("batchId", batch.getId())
.setParameter(
"empIds", employees.stream().map(EmployeeEntity::getId).toList())
.getResultList();
if (!exists.isEmpty()) {
for (EmpSalaryEntity exist : exists) {
log.warn("批次 {} 员工 {} 工资数据已存在", batch.getId(), exist.getEmployee().getName());
}
throw new BizException(
"部分员工工资数据已存在: {}",
exists.stream()
.map(e -> e.getEmployee().getName())
.collect(Collectors.joining(",")));
}
}
/**
* 构建计算上下文
*
* @param batch
* @param employee
* @param attendance
* @param salary
* @return
*/
public Map<String, Object> buildCtx(
EmpSalaryBatchEntity batch,
EmployeeEntity employee,
EmpMonthAttendanceEntity attendance,
EmpSalaryEntity salary) {
Map<String, Object> empMap =
employee == null ? new HashMap<>() : ReflectUtils.toMap(employee);
if (employee != null) {
empMap.put("empName", employee.getName());
empMap.put("empCode", employee.getCode());
}
Map<String, Object> batchMap = batch == null ? new HashMap<>() : ReflectUtils.toMap(batch);
Map<String, Object> attendanceMap =
attendance == null ? new HashMap<>() : ReflectUtils.toMap(attendance);
Map<String, Object> salaryMap =
ReflectUtils.toMap(salary == null ? new EmpSalaryEntity() : salary);
Map<String, Object> ctx = new HashMap<>();
ctx.putAll(salaryMap);
ctx.putAll(empMap);
ctx.putAll(batchMap);
ctx.putAll(attendanceMap);
// 移除通用字段
ReflectUtils.getAllFieldsList(OrgCommonEntity.class)
.forEach(
f -> {
ctx.remove(f.getName());
});
return ctx;
}
}

View File

@@ -73,48 +73,53 @@ public class EmpSalaryItemService
@PersistenceContext EntityManager em;
public List<EmpSalaryItemEntity> findAllBy(String organizationId, List<String> dependOn, List<String> dependOnSysItem) {
public List<EmpSalaryItemEntity> findAllBy(
String organizationId, List<String> dependOn, List<String> dependOnSysItem) {
Map<String, Object> params = Map.of("organizationId", organizationId, "dependOn", dependOn, "dependOnSysItem", dependOnSysItem);
List<String> ids = JpaUtils.execNativeQuery(
em,
FreeMakerUtils.render(
"""
Map<String, Object> params =
Map.of(
"organizationId",
organizationId,
"dependOn",
dependOn,
"dependOnSysItem",
dependOnSysItem);
List<String> ids =
JpaUtils.execNativeQuery(
em,
FreeMakerUtils.render(
"""
select * from t_emp_salary_item
<@where>
<#if organizationId??>
AND organization_id = :organizationId
</#if>
<#if dependOn?? && !dependOnSysItem??>
AND depend_on @> ARRAY[:dependOn]
</#if>
<#if dependOnSysItem?? && !dependOn??>
AND depend_on_sys_item @> ARRAY[:dependOnSysItem]
</#if>
<#if dependOn?? && dependOnSysItem??>
AND (depend_on @> ARRAY[:dependOn] or depend_on_sys_item @> ARRAY[:dependOnSysItem])
</#if>
</@where>
""",
params),
params,
String.class);
params),
params,
String.class);
return findAllByIds(ids);
}
@@ -206,7 +211,7 @@ public class EmpSalaryItemService
AtomicReference<String> formulaAtomic = new AtomicReference<>(item.getFormulaShow());
List<String> dependOn = new ArrayList<>();
item.setDependOn(dependOn);
item.setDependOn(new ArrayList<>());
item.setDependOnSysItem(new ArrayList<>());
@@ -221,6 +226,7 @@ public class EmpSalaryItemService
&& formulaAtomic.get().contains(x.getName())) {
// 刷新依赖关系
dependOn.add(x.getCode());
item.getDependOn().add(x.getCode());
// 把表达式中的名称替换为代码
formulaAtomic.set(
@@ -235,6 +241,7 @@ public class EmpSalaryItemService
if (StringUtils.isNotEmpty(x.getName())
&& formulaAtomic.get().contains(x.getName())) {
// 刷新依赖关系
dependOn.add(x.getCode());
item.getDependOnSysItem().add(x.getCode());
// 把表达式中的名称替换为代码
@@ -244,6 +251,7 @@ public class EmpSalaryItemService
});
item.setDependOn(item.getDependOn().stream().distinct().toList());
item.setDependOnSysItem(item.getDependOnSysItem().stream().distinct().toList());
String formula = formulaAtomic.get();
List<String> variables = new ArrayList<>();
@@ -438,11 +446,17 @@ public class EmpSalaryItemService
.filter(x -> !StringUtils.equalsAny(x.getCode(), "yfheji", "kfheji"))
.forEach(
x -> {
if (x.getDependOn()!=null && x.getDependOn().contains(entity.getCode())) {
throw new BizException("[%s] 依赖于 [%s], 请先禁用 [%s] 或者删除公式中的 [%s]".formatted(x.getName(), entity.getName(), x.getName(), entity.getName()));
if (x.getDependOn() != null
&& x.getDependOn().contains(entity.getCode())) {
throw new BizException(
"[%s] 依赖于 [%s], 请先禁用 [%s] 或者删除公式中的 [%s]"
.formatted(
x.getName(),
entity.getName(),
x.getName(),
entity.getName()));
}
});
}
this.mapper.updateEntity(entity, request);
@@ -677,6 +691,19 @@ public class EmpSalaryItemService
x -> ObjectUtils.defaultIfNull(x.getSortKey(), 0)))
.toList();
List<EmpSalarySysItemEntity> functionItems =
sysItems.stream().filter(x -> StringUtils.equals(x.getItemType(), "2")).toList();
if (CollectionUtils.isNotEmpty(functionItems)) {
sysItemScript.append("// 系统预设函数开始\n");
for (EmpSalarySysItemEntity functionItem : functionItems) {
sysItemScript.append(functionItem.getItemExpression().trim()).append("\n");
}
sysItemScript.append("// 系统预设函数结束\n");
}
if (CollectionUtils.isNotEmpty(usedSysItems)) {
sysItemScript.append("// 系统预设项目开始\n");
@@ -688,13 +715,9 @@ public class EmpSalaryItemService
sysItemScript
.append(sysItem.getCode())
.append(" = ")
.append(sysItem.getItemExpression())
.append(sysItem.getItemExpression().trim())
.append(";\n");
}
case "2" -> {
sysItemScript.append(sysItem.getItemExpression()).append(";\n");
}
}
}