This commit is contained in:
2024-08-15 22:11:28 +08:00
parent fd6904218a
commit cefaf53804
14 changed files with 813 additions and 278 deletions

View File

@@ -0,0 +1,184 @@
package cn.lihongjie.coal.common;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.CaffeineSpec;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.Script;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;
@Slf4j
public class GroovyScriptManager {
private static final AtomicInteger counter = new AtomicInteger(0);
private final String name;
private final Cache<String, Class> scriptCache;
private final GroovyClassLoader groovyClassLoader;
private boolean logCompileTime = false;
public GroovyScriptManager() {
this(true);
}
public GroovyScriptManager(boolean logCompileTime) {
this((c) -> {}, logCompileTime);
}
public GroovyScriptManager(
Consumer<CompilerConfiguration> configCustomizer, boolean logCompileTime) {
this(null, configCustomizer, logCompileTime);
}
public GroovyScriptManager(
String name, Consumer<CompilerConfiguration> configCustomizer, boolean logCompileTime) {
this(name, "maximumSize=1000,expireAfterAccess=1h", configCustomizer, logCompileTime);
}
public GroovyScriptManager(
String name,
String specification,
Consumer<CompilerConfiguration> configConsumer,
boolean logCompileTime) {
this.name = StringUtils.defaultIfBlank(name, "GroovyScriptManager-" + counter.get());
scriptCache = Caffeine.from(CaffeineSpec.parse(specification)).build();
CompilerConfiguration config = new CompilerConfiguration();
configConsumer.accept(config);
this.groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader(), config);
this.logCompileTime = logCompileTime;
counter.incrementAndGet();
log.info("GroovyScriptManager: {} created", name);
}
/** 高性能版本API, 允许自定义缓存key */
public Class compile(String key, Supplier<String> scriptSupplier) {
StringBuilder logText = new StringBuilder();
logText.append("GroovyScriptManager: ").append(name).append("\n");
logText.append("compile groovy script: ").append(key).append("\n");
long start = System.currentTimeMillis();
try {
return scriptCache.get(
key,
k -> {
long getStart = System.currentTimeMillis();
String script = scriptSupplier.get();
long getEnd = System.currentTimeMillis();
logText.append("getScriptTime: ").append(getEnd - getStart).append("ms\n");
GroovyCodeSource codeSource =
new GroovyCodeSource(script, key, "/groovy/script");
codeSource.setCachable(false);
long parseStart = System.currentTimeMillis();
Class parsedClass = this.groovyClassLoader.parseClass(codeSource);
long parseEnd = System.currentTimeMillis();
logText.append("parseTime: ").append(parseEnd - parseStart).append("ms\n");
return parsedClass;
});
} finally {
long end = System.currentTimeMillis();
logText.append("totalTime: ").append(end - start).append("ms");
}
}
public Script newInstance(String key, Supplier<String> scriptSupplier) {
Class aClass = compile(key, scriptSupplier);
Object o = ReflectUtils.newInstance(aClass);
if (o instanceof Script) {
return (Script) o;
} else {
throw new IllegalArgumentException(
"script must be instance of groovy.lang.Script, get " + aClass.getName());
}
}
public void runWithScript(
String key, Supplier<String> scriptSupplier, Consumer<Script> scriptCustomizer) {
Script script = newInstance(key, scriptSupplier);
scriptCustomizer.accept(script);
script.run();
}
/** 易用性API, key 为 脚本内容的md5 */
public Class compile(Supplier<String> scriptSupplier) {
return compile(
DigestUtils.md5DigestAsHex(scriptSupplier.get().getBytes(StandardCharsets.UTF_8)),
scriptSupplier);
}
public Class compile(String script) {
return compile(
DigestUtils.md5DigestAsHex(script.getBytes(StandardCharsets.UTF_8)), () -> script);
}
public Script newInstance(Supplier<String> scriptSupplier) {
return newInstance(
DigestUtils.md5DigestAsHex(scriptSupplier.get().getBytes(StandardCharsets.UTF_8)),
scriptSupplier);
}
public Script newInstance(String script) {
return newInstance(
DigestUtils.md5DigestAsHex(script.getBytes(StandardCharsets.UTF_8)), () -> script);
}
public void runWithScript(Supplier<String> scriptSupplier, Consumer<Script> scriptCustomizer) {
runWithScript(
DigestUtils.md5DigestAsHex(scriptSupplier.get().getBytes(StandardCharsets.UTF_8)),
scriptSupplier,
scriptCustomizer);
}
public void runWithScript(String script, Consumer<Script> scriptCustomizer) {
runWithScript(
DigestUtils.md5DigestAsHex(script.getBytes(StandardCharsets.UTF_8)),
() -> script,
scriptCustomizer);
}
public void invalidateAll() {
scriptCache.invalidateAll();
}
public void invalidate(String key) {
scriptCache.invalidate(key);
}
@SneakyThrows
public void destroy() {
scriptCache.invalidateAll();
groovyClassLoader.close();
}
}

View File

@@ -0,0 +1,41 @@
package cn.lihongjie.coal.common;
import lombok.experimental.UtilityClass;
import java.util.*;
import java.util.function.Predicate;
import java.util.function.Supplier;
@UtilityClass
public class MapUtils {
public static <K, V> Map<K, V> keepKey(Map<K, V> src, Collection<K> keys) {
return keepOnlyKeys(src, keys::contains, HashMap::new);
}
public static <K, V> Map<K, V> keepOnlyKeys(
Map<K, V> src, Predicate<K> predicate, Supplier<Map<K, V>> mapSupplier) {
Map<K, V> result = mapSupplier.get();
for (Map.Entry<K, V> entry : src.entrySet()) {
if (predicate.test(entry.getKey())) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
public static <K, V> void merge(Map<K, V> src, Map<K, V> target, Predicate<K> predicate) {
for (Map.Entry<K, V> entry : src.entrySet()) {
if (predicate.test(entry.getKey())) {
target.put(entry.getKey(), entry.getValue());
}
}
}
public static <K, V> void merge(Map<K, V> src, Map<K, V> target, Collection<K> keys) {
merge(src, target, keys::contains);
}
}

View File

@@ -9,6 +9,8 @@ import cn.lihongjie.coal.empMonthAttendance.dto.UpdateEmpMonthAttendanceDto;
import cn.lihongjie.coal.empMonthAttendance.entity.EmpMonthAttendanceEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.control.DeepClone;
@Mapper(
@@ -22,4 +24,9 @@ public interface EmpMonthAttendanceMapper
CreateEmpMonthAttendanceDto,
UpdateEmpMonthAttendanceDto> {
CreateEmpMonthAttendanceDto toCreateDto(UpdateEmpMonthAttendanceDto dto);
@Mappings(
@Mapping(target = "employee", ignore = true)
)
EmpMonthAttendanceDto toDto2(EmpMonthAttendanceEntity empMonthAttendance);
}

View File

@@ -3,6 +3,8 @@ package cn.lihongjie.coal.empSalary.dto;
import cn.lihongjie.coal.base.dto.OrgCommonDto;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -13,9 +15,13 @@ public class CreateEmpSalaryDto extends OrgCommonDto {
@ManyToOne
@NotNull
@NotEmpty
private String batch;
@ManyToOne
@NotNull
@NotEmpty
private String employee;

View File

@@ -1,9 +1,9 @@
package cn.lihongjie.coal.empSalary.dto;
import cn.lihongjie.coal.base.dto.OrgCommonDto;
import cn.lihongjie.coal.base.dto.SimpleDto;
import cn.lihongjie.coal.common.DictCode;
import cn.lihongjie.coal.empSalaryBatch.dto.EmpSalaryBatchDto;
import cn.lihongjie.coal.employee.dto.EmployeeDto;
import cn.lihongjie.coal.pojoProcessor.DictTranslate;
import jakarta.persistence.ManyToOne;
@@ -26,7 +26,7 @@ public class EmpSalaryDto extends OrgCommonDto {
private EmpSalaryBatchDto batch;
@ManyToOne
private EmployeeDto employee;
private SimpleDto employee;
@Comment("员工快照")

View File

@@ -3,6 +3,8 @@ package cn.lihongjie.coal.empSalary.dto;
import cn.lihongjie.coal.base.dto.OrgCommonDto;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -13,9 +15,13 @@ public class UpdateEmpSalaryDto extends OrgCommonDto {
@ManyToOne
@NotNull
@NotEmpty
private String batch;
@ManyToOne
@NotNull
@NotEmpty
private String employee;

View File

@@ -8,6 +8,7 @@ import cn.lihongjie.coal.employee.entity.EmployeeEntity;
import cn.lihongjie.coal.pojoProcessor.DictTranslate;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import lombok.Data;
@@ -33,11 +34,14 @@ public class EmpSalaryEntity extends OrgCommonEntity {
"item41", "item42", "item43", "item44", "item45", "item46", "item47", "item48",
"item49", "item50", "yfheji", "kfheji", "sfheji");
@ManyToOne private EmpSalaryBatchEntity batch;
@ManyToOne(fetch = FetchType.LAZY)
private EmpSalaryBatchEntity batch;
@ManyToOne private EmployeeEntity employee;
@ManyToOne(fetch = FetchType.LAZY)
private EmployeeEntity employee;
@ManyToOne private EmpMonthAttendanceEntity empMonthAttendance;
@ManyToOne(fetch = FetchType.LAZY)
private EmpMonthAttendanceEntity empMonthAttendance;
private BigDecimal item0;
private BigDecimal item1;

View File

@@ -5,10 +5,12 @@ import cn.lihongjie.coal.base.mapper.CommonEntityMapper;
import cn.lihongjie.coal.base.mapper.CommonMapper;
import cn.lihongjie.coal.empSalary.dto.CreateEmpSalaryDto;
import cn.lihongjie.coal.empSalary.dto.EmpSalaryDto;
import cn.lihongjie.coal.empSalary.dto.RecalculatePreviewDto;
import cn.lihongjie.coal.empSalary.dto.UpdateEmpSalaryDto;
import cn.lihongjie.coal.empSalary.entity.EmpSalaryEntity;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.control.DeepClone;
@Mapper(
@@ -16,4 +18,6 @@ import org.mapstruct.control.DeepClone;
uses = {CommonMapper.class, CommonEntityMapper.class},
mappingControl = DeepClone.class)
public interface EmpSalaryMapper
extends BaseMapper<EmpSalaryEntity, EmpSalaryDto, CreateEmpSalaryDto, UpdateEmpSalaryDto> {}
extends BaseMapper<EmpSalaryEntity, EmpSalaryDto, CreateEmpSalaryDto, UpdateEmpSalaryDto> {
void updateItems(RecalculatePreviewDto recDto, @MappingTarget UpdateEmpSalaryDto request);
}

View File

@@ -5,6 +5,7 @@ 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.MapUtils;
import cn.lihongjie.coal.common.ReflectUtils;
import cn.lihongjie.coal.empMonthAttendance.dto.EmpMonthAttendanceDto;
import cn.lihongjie.coal.empMonthAttendance.entity.EmpMonthAttendanceEntity;
@@ -19,8 +20,10 @@ import cn.lihongjie.coal.empSalaryBatch.mapper.EmpSalaryBatchMapper;
import cn.lihongjie.coal.empSalaryBatch.service.EmpSalaryBatchService;
import cn.lihongjie.coal.empSalaryItem.entity.EmpSalaryItemEntity;
import cn.lihongjie.coal.empSalaryItem.service.EmpSalaryItemService;
import cn.lihongjie.coal.employee.dto.EmployeeCalculateDto;
import cn.lihongjie.coal.employee.dto.EmployeeDto;
import cn.lihongjie.coal.employee.entity.EmployeeEntity;
import cn.lihongjie.coal.employee.mapper.EmployeeMapper;
import cn.lihongjie.coal.employee.service.EmployeeService;
import cn.lihongjie.coal.exception.BizException;
@@ -70,12 +73,7 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
@Autowired private ConversionService conversionService;
public EmpSalaryDto create(CreateEmpSalaryDto request) {
EmpSalaryEntity entity = mapper.toEntity(request);
this.repository.save(entity);
return getById(entity.getId());
}
@Autowired EmployeeMapper employeeMapper;
@PersistenceContext EntityManager em;
@Autowired RedissonClient redissonClient;
@@ -108,11 +106,35 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
@Autowired EmpSalaryItemService empSalaryItemService;
private static void assertBatchEditable(EmpSalaryBatchEntity salaryBatch) {
if (salaryBatch.getStatus() != 1) {
throw new BizException("批次不属于编辑状态, 无法编辑或删除");
}
}
private static void removeCommonField(Map<String, Object> ctx) {
ReflectUtils.getAllFieldsList(OrgCommonEntity.class)
.forEach(
f -> {
ctx.remove(f.getName());
});
}
public EmpSalaryDto create(CreateEmpSalaryDto request) {
assertBatchEditable(request.getBatch());
EmpSalaryEntity entity = mapper.toEntity(request);
this.repository.save(entity);
return getById(entity.getId());
}
public EmpSalaryDto update(UpdateEmpSalaryDto request) {
assertBatchEditable(request.getBatch());
var recDto = recalculatePreview(request);
updateItems(recDto, request);
this.mapper.updateItems(recDto, request);
EmpSalaryEntity entity = this.repository.get(request.getId());
if (this.repository.containArchived(request.getId())) {
@@ -125,21 +147,6 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
return getById(entity.getId());
}
private void updateItems(RecalculatePreviewDto recDto, UpdateEmpSalaryDto request) {
for (String key : EmpSalaryEntity.ITEM_KEYS) {
ReflectUtils.writeField(
request, key, conversionService.convert(ReflectUtils.readField(recDto, key), BigDecimal.class));
}
}
public void delete(IdRequest request) {
if (this.repository.containArchived(request)) {
throw new BizException("部分数据已归档,无法编辑或删除");
}
this.repository.deleteAllById(request.getIds());
}
@Autowired EmpSalaryBatchService batchService;
@Autowired EmployeeService employeeService;
@@ -193,47 +200,23 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
.getResultList();
}
/**
* 按照ID进行重新计算
*
* @param ids
*/
public void recalculate(IdRequest ids) {
private void assertBatchEditable(String batch) {
List<EmpSalaryEntity> salaries =
em.createQuery(
"select s from EmpSalaryEntity s join fetch s.employee join fetch s.batch join fetch s.empMonthAttendance where s.id in :ids",
EmpSalaryEntity.class)
.setParameter("ids", ids.getIds())
.getResultList();
EmpSalaryBatchEntity salaryBatch = batchService.get(batch);
recalculate(salaries, true);
assertBatchEditable(salaryBatch);
}
/** 针对某个员工重新计算, 用于编辑表单的预览 */
@SneakyThrows
public RecalculatePreviewDto recalculatePreview(UpdateEmpSalaryDto dto) {
public void delete(IdRequest request) {
EmpSalaryBatchEntity batch = this.batchService.get(dto.getBatch());
List<EmpSalaryEntity> all = this.findAllByIds(request.getIds());
EmpSalaryEntity salary = this.get(dto.getId());
all.stream().map(x -> x.getBatch().getId()).distinct().forEach(this::assertBatchEditable);
GroovyClassLoader groovyClassLoader = null;
groovyClassLoader = initClassLoader(groovyClassLoader);
Script scriptObj = initScriptClass(groovyClassLoader, genScript(batch));
Map<String, Object> ctx = buildCtx(null, null, null, salary);
// 覆盖工资项目
overWriteSalaryItem(ctx, ReflectUtils.toMap(dto));
scriptObj.setBinding(new Binding(Map.of("salary", ctx)));
scriptObj.run();
return buildDto(ctx);
if (this.repository.containArchived(request)) {
throw new BizException("部分数据已归档,无法编辑或删除");
}
this.repository.deleteAllById(request.getIds());
}
private RecalculatePreviewDto buildDto(Map<String, Object> ctx) {
@@ -264,6 +247,57 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
}
}
/**
* 按照ID进行重新计算
*
* @param ids
*/
public void recalculate(IdRequest ids) {
List<EmpSalaryEntity> salaries =
em.createQuery(
"""
select s from EmpSalaryEntity s
left join fetch s.employee e
left join fetch e.department
left join fetch e.jobPost
left join fetch s.batch
left join fetch s.empMonthAttendance
where s.id in :ids""",
EmpSalaryEntity.class)
.setParameter("ids", ids.getIds())
.getResultList();
salaries.stream().map(x -> x.getBatch().getId()).distinct().forEach(this::assertBatchEditable);
recalculate(salaries, true);
}
/** 针对某个员工重新计算, 用于编辑表单的预览 */
@SneakyThrows
public RecalculatePreviewDto recalculatePreview(UpdateEmpSalaryDto dto) {
EmpSalaryBatchEntity batch = this.batchService.get(dto.getBatch());
EmpSalaryEntity salary = this.get(dto.getId());
Script scriptObj = empSalaryItemService.newScriptInstance(batch.getOrganizationId());
Map<String, Object> ctx = buildPreviewCtx(dto, salary);
// 覆盖工资项目
overWriteSalaryItem(ctx, ReflectUtils.toMap(dto));
scriptObj.setBinding(new Binding(Map.of("salary", ctx)));
scriptObj.run();
return buildDto(ctx);
}
/** 重新计算 */
@SneakyThrows
public void recalculate(List<EmpSalaryEntity> salaries, boolean update) {
@@ -274,7 +308,7 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
EmpSalaryBatchEntity batch = salaries.get(0).getBatch();
StopWatch stopWatch = new StopWatch("initSalary: " + batch.getId());
StopWatch stopWatch = new StopWatch("recalculate: " + batch.getId());
stopWatch.start("getLock");
RLock lock = redissonClient.getLock("batchModify." + batch.getId());
@@ -295,26 +329,19 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
checkBatchStatus(batch);
stopWatch.stop();
// 查询考勤数据
stopWatch.start("genScript");
// 生成计算脚本
String script = genScript(batch);
stopWatch.stop();
stopWatch.start("parseScript");
groovyClassLoader = initClassLoader(groovyClassLoader);
Script scriptObj = initScriptClass(groovyClassLoader, script);
Script scriptObj = empSalaryItemService.newScriptInstance(batch.getOrganizationId());
stopWatch.stop();
List<EmployeeDto> dtoByIds =
this.employeeService.getDtoByIds(
salaries.stream().map(e -> e.getEmployee().getId()).toList());
var dtoByIds = salaries.stream().map(x -> x.getEmployee()).map(employeeMapper::toCalculateDto).toList();
Map<String, EmployeeDto> dtoMap =
Map<String, EmployeeCalculateDto> dtoMap =
dtoByIds.stream().collect(Collectors.toMap(e -> e.getId(), e -> e));
// 执行计算脚本
@@ -323,11 +350,9 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
var employee = salary.getEmployee();
EmpMonthAttendanceEntity attendance = salary.getEmpMonthAttendance();
stopWatch.start("buildCtx: " + employee.getName());
Map<String, Object> ctx =
buildCtx(batch, dtoMap.get(employee.getId()), attendance, salary);
Map<String, Object> ctx = buildRecalculateCtx(salary);
stopWatch.stop();
@@ -347,7 +372,6 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
salary.update(salaryEntity);
stopWatch.stop();
// salaries.add(salaryEntity);
}
// 保存到数据库
@@ -380,11 +404,10 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
public void initSalary(InitSalaryDto initSalaryDto) {
EmpSalaryBatchEntity batch = batchService.get(initSalaryDto.getBatchId());
List<EmployeeDto> employees = employeeService.getDtoByIds(initSalaryDto.getEmployeesIds());
StopWatch stopWatch = new StopWatch("initSalary: " + batch.getId());
var employees = employeeService.getCalculateDtoByIds(initSalaryDto.getEmployeesIds());
stopWatch.start("getLock");
RLock lock = redissonClient.getLock("batchModify." + batch.getId());
@@ -394,7 +417,6 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
if (!tryLock) {
batchModifing(batch);
}
GroovyClassLoader groovyClassLoader = null;
try {
@@ -418,12 +440,6 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
stopWatch.stop();
stopWatch.start("genScript");
// 生成计算脚本
String script = genScript(batch);
stopWatch.stop();
stopWatch.start("getItems");
// 获取工资项
List<EmpSalaryItemEntity> items =
@@ -434,16 +450,14 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
.filter(
x ->
BooleanUtils.isTrue(x.getInherit())
&& StringUtils.equalsAny(x.getInputType(), "0"))
&& StringUtils.equalsAny(x.getInputType(), "1"))
.filter(x -> ObjectUtils.notEqual(x.getStatus(), 0))
.toList();
stopWatch.stop();
stopWatch.start("parseScript");
groovyClassLoader = initClassLoader(groovyClassLoader);
Script scriptObj = initScriptClass(groovyClassLoader, script);
stopWatch.start("newScriptInstance");
Script scriptObj = empSalaryItemService.newScriptInstance(batch.getOrganizationId());
stopWatch.stop();
@@ -463,7 +477,7 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
List<EmpSalaryEntity> salaries = new ArrayList<>();
for (EmployeeDto employee : employees) {
for (var employee : employees) {
EmpMonthAttendanceEntity attendance =
attMap.containsKey(employee.getId())
@@ -471,12 +485,9 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
: null;
stopWatch.start("buildCtx: " + employee.getName());
Map<String, Object> ctx = buildCtx(batch, employee, attendance, null);
stopWatch.stop();
// todo 处理继承项
Map<String, Object> ctx = buildInitCtx(batch, employee, attendance);
// 继承历史工资数据
if (salaryHisMap.containsKey(employee.getId())
&& CollectionUtils.isNotEmpty(inheritItems)) {
@@ -488,6 +499,7 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
salaryHisMap.get(employee.getId()), inheritItem.getCode()));
}
}
stopWatch.stop();
// 计算
@@ -521,14 +533,114 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
stopWatch.stop();
}
if (groovyClassLoader != null) {
groovyClassLoader.close();
}
log.info(stopWatch.prettyPrint());
}
}
public Map<String, Object> buildInitCtx(
EmpSalaryBatchEntity batchEntity,
EmployeeCalculateDto employeeEntity,
EmpMonthAttendanceEntity monthAttendanceEntity) {
var employeeCtx = buildCtx(employeeEntity);
var batchCtx = buildCtx(batchEntity);
var empMonthAttendanceCtx = buildCtx(monthAttendanceEntity);
var salaryCtx = new HashMap<String, Object>();
HashMap<String, Object> map = new HashMap<>();
map.putAll(salaryCtx);
map.putAll(employeeCtx);
map.putAll(batchCtx);
map.putAll(empMonthAttendanceCtx);
return map;
}
/**
* 编辑预览的时候构建上下文 从数据库中取所有冗余数据 + 前端传过来的工资数据
*
* @return
*/
public Map<String, Object> buildPreviewCtx(UpdateEmpSalaryDto dto, EmpSalaryEntity entity) {
EmpSalaryDto salaryDto = this.mapper.toDto(entity);
Map<String, Object> userInput = ReflectUtils.toMap(dto);
Map<String, Object> ctx = ReflectUtils.toMap(salaryDto);
MapUtils.merge(userInput, ctx, EmpSalaryEntity.ITEM_KEYS);
return ctx;
}
public Map<String, Object> buildRecalculateCtx(EmpSalaryEntity entity) {
var employeeCtx = buildCtx(employeeMapper.toCalculateDto(entity.getEmployee()));
var batchCtx = buildCtx(entity.getBatch());
var empMonthAttendanceCtx = buildCtx(entity.getEmpMonthAttendance());
var salaryCtx = buildCtx(entity, true);
HashMap<String, Object> map = new HashMap<>();
map.putAll(salaryCtx);
map.putAll(employeeCtx);
map.putAll(batchCtx);
map.putAll(empMonthAttendanceCtx);
return map;
}
private Map<String, Object> buildCtx(EmpSalaryEntity entity, boolean onlyItems) {
if (entity == null) {
return new HashMap<>();
}
EmpSalaryDto salary = empSalaryMapper.toDto(entity);
Map<String, Object> map = ReflectUtils.toMap(salary);
if (onlyItems) {
return MapUtils.keepKey(map, EmpSalaryEntity.ITEM_KEYS);
} else {
removeCommonField(map);
return map;
}
}
private Map<String, Object> buildCtx(EmpMonthAttendanceEntity empMonthAttendance) {
if (empMonthAttendance == null) {
return new HashMap<>();
}
var attendance = empMonthAttendanceMapper.toDto2(empMonthAttendance);
Map<String, Object> map = ReflectUtils.toMap(attendance);
map.put("attendanceId", empMonthAttendance.getId());
removeCommonField(map);
return map;
}
private Map<String, Object> buildCtx(EmpSalaryBatchEntity batch) {
if (batch == null) {
return new HashMap<>();
}
var batchDto = empSalaryBatchMapper.toDto(batch);
Map<String, Object> batchMap = ReflectUtils.toMap(batchDto);
batchMap.put("batchId", batch.getId());
removeCommonField(batchMap);
return batchMap;
}
@Autowired EmpMonthAttendanceMapper empMonthAttendanceMapper;
private String genScript(EmpSalaryBatchEntity batch) {
@@ -544,13 +656,38 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
return script;
}
private Map<String, Object> buildCtx(EmployeeCalculateDto employee) {
if (employee == null) {
return new HashMap<>();
}
Map<String, Object> empMap = ReflectUtils.toMap(employee);
empMap.put("empId", employee.getId());
empMap.put("empName", employee.getName());
empMap.put("empCode", employee.getCode());
Try.run(() -> empMap.put("departmentId", employee.getDepartment().getId()));
Try.run(() -> empMap.put("departmentCode", employee.getDepartment().getCode()));
Try.run(() -> empMap.put("departmentName", employee.getDepartment().getName()));
Try.run(() -> empMap.put("jobPostId", employee.getJobPost().getId()));
Try.run(() -> empMap.put("jobPostCode", employee.getJobPost().getCode()));
Try.run(() -> empMap.put("jobPostName", employee.getJobPost().getName()));
removeCommonField(empMap);
return empMap;
}
private @NotNull Map<String, List<EmpMonthAttendanceEntity>> queryAttendanceMap(
EmpSalaryBatchEntity batch, List<EmployeeDto> employees) {
EmpSalaryBatchEntity batch, List<EmployeeCalculateDto> 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(EmployeeDto::getId).toList())
.setParameter(
"empIds",
employees.stream().map(EmployeeCalculateDto::getId).toList())
.setParameter("yearMonth", batch.getBatchYearMonth())
.getResultList();
@@ -559,29 +696,11 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
return attMap;
}
private void checkBatchStatus(EmpSalaryBatchEntity batch) {}
private void checkBatchStatus(EmpSalaryBatchEntity batch) {
private void checkDuplicate(EmpSalaryBatchEntity batch, List<EmployeeDto> 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(EmployeeDto::getId).toList())
.getResultList();
if (!exists.isEmpty()) {
assertBatchEditable(batch);
for (EmpSalaryEntity exist : exists) {
log.warn("批次 {} 员工 {} 工资数据已存在", batch.getId(), exist.getEmployee().getName());
}
throw new BizException(
"部分员工工资数据已存在: {}",
exists.stream()
.map(e -> e.getEmployee().getName())
.collect(Collectors.joining(",")));
}
}
@Autowired EmpSalaryMapper empSalaryMapper;
@@ -595,16 +714,52 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
return groovyClassLoader;
}
private void checkDuplicate(EmpSalaryBatchEntity batch, List<EmployeeCalculateDto> 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(EmployeeCalculateDto::getId).toList())
.getResultList();
if (!exists.isEmpty()) {
for (EmpSalaryEntity exist : exists) {
log.warn("批次 {} 员工 {} 工资数据已存在", batch.getName(), exist.getEmployee().getName());
}
throw new BizException(
"部分员工工资数据已存在: {}",
exists.stream()
.map(e -> e.getEmployee().getName())
.collect(Collectors.joining(",")));
}
}
private List<EmpSalaryEntity> queryHisSalary(List<String> employeesIds) {
return new ArrayList<>();
// return this.em
// .createQuery(
// "select s from EmpSalaryEntity s where s.employee.id in :empIds
// and rank() over (partition by s.employee.id order by s.createTime desc ) = 1",
// EmpSalaryEntity.class)
// .setParameter("empIds", employeesIds)
// .getResultList();
em.createNativeQuery(
"""
with tmp1 as (
select s.id, rank() over (partition by employee_id order by batch_year_month desc ) rk from t_emp_salary s where s.employee_id in :ids
)
select id from tmp1 where rk = 1
""",
String.class)
.setParameter("ids", employeesIds)
.getResultList();
return em.createQuery(
"select s from EmpSalaryEntity s where s.employee.id in :ids",
EmpSalaryEntity.class)
.setParameter("ids", employeesIds)
.getResultList();
}
/**
@@ -659,11 +814,7 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
// 移除通用字段
ReflectUtils.getAllFieldsList(OrgCommonEntity.class)
.forEach(
f -> {
ctx.remove(f.getName());
});
removeCommonField(ctx);
return ctx;
}
@@ -674,15 +825,4 @@ public class EmpSalaryService extends BaseService<EmpSalaryEntity, EmpSalaryRepo
log.info("删除工资数据 {} 条", cnt);
}
public void sendToAudit(IdRequest request) {
EmpSalaryBatchEntity batch = this.batchService.get(request.getId());
if (!StringUtils.equalsAny(batch.getBatchStatus(), "1")) {
throw new BizException("批次状态不正确");
}
batch.setBatchStatus("2");
}
}

View File

@@ -3,6 +3,8 @@ package cn.lihongjie.coal.empSalaryItem.repository;
import cn.lihongjie.coal.base.dao.BaseRepository;
import cn.lihongjie.coal.empSalaryItem.entity.EmpSalaryItemEntity;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
@@ -11,4 +13,17 @@ public interface EmpSalaryItemRepository extends BaseRepository<EmpSalaryItemEnt
long countByNameContainsAndOrganizationIdAndIdNot(
String name, String organizationId, String id);
@Query(
value =
"""
select md5(string_agg(t.f, ',')) from (
select f from (
select code ,formula f from t_emp_salary_item where organization_id = :organizationId
union all
select code, item_expression f from t_emp_salary_sys_item
) as t order by code asc) as t
""",
nativeQuery = true)
String computeCacheKey(@Param("organizationId") String organizationId);
}

View File

@@ -4,27 +4,27 @@ import cn.lihongjie.coal.base.dto.CommonQuery;
import cn.lihongjie.coal.base.dto.IdRequest;
import cn.lihongjie.coal.base.service.BaseService;
import cn.lihongjie.coal.common.Ctx;
import cn.lihongjie.coal.empSalary.entity.EmpSalaryEntity;
import cn.lihongjie.coal.empSalaryBatch.entity.EmpSalaryBatchEntity;
import cn.lihongjie.coal.common.GroovyScriptManager;
import cn.lihongjie.coal.empSalaryItem.dto.CreateEmpSalaryItemDto;
import cn.lihongjie.coal.empSalaryItem.dto.EmpSalaryItemDto;
import cn.lihongjie.coal.empSalaryItem.dto.UpdateEmpSalaryItemDto;
import cn.lihongjie.coal.empSalaryItem.entity.EmpSalaryItemEntity;
import cn.lihongjie.coal.empSalaryItem.mapper.EmpSalaryItemMapper;
import cn.lihongjie.coal.empSalaryItem.repository.EmpSalaryItemRepository;
import cn.lihongjie.coal.empSalaryStandard.dto.EmpSalaryStandardResult;
import cn.lihongjie.coal.empSalaryStandard.service.EmpSalaryStandardService;
import cn.lihongjie.coal.empSalarySysItem.entity.EmpSalarySysItemEntity;
import cn.lihongjie.coal.empSalarySysItem.service.EmpSalarySysItemService;
import cn.lihongjie.coal.exception.BizException;
import groovy.lang.GroovyClassLoader;
import groovy.lang.Closure;
import groovy.lang.Script;
import io.vavr.Tuple;
import io.vavr.Tuple2;
import io.vavr.collection.Stream;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.CriteriaBuilder;
@@ -32,7 +32,6 @@ import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
@@ -43,6 +42,7 @@ 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.jgrapht.alg.cycle.CycleDetector;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
@@ -56,8 +56,10 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@@ -74,6 +76,7 @@ public class EmpSalaryItemService
@Autowired private EmpSalaryStandardService empSalaryStandardService;
@PersistenceContext EntityManager em;
private GroovyScriptManager groovyScriptManager;
public List<EmpSalaryItemEntity> findAllBy(
String organizationId, List<String> dependOn, List<String> dependOnSysItem) {
@@ -176,6 +179,47 @@ public class EmpSalaryItemService
@Autowired EmpSalarySysItemService empSalarySysItemService;
@PostConstruct
public void init() {
groovyScriptManager =
new GroovyScriptManager(
"salaryItemScript",
new Consumer<CompilerConfiguration>() {
@Override
public void accept(CompilerConfiguration compilerConfiguration) {
compilerConfiguration.addCompilationCustomizers(
new SecureASTCustomizer() {
{
setAllowedImports(List.of("java.lang.Math"));
setAllowedReceiversClasses(
Arrays.asList(
Object[].class,
Arrays.class,
java.math.RoundingMode.class,
Closure.class,
Map.class,
Object.class,
Math.class,
Integer.class,
Float.class,
Double.class,
Long.class,
BigDecimal.class));
}
});
}
},
true);
}
@PreDestroy
public void destroy() {
groovyScriptManager.destroy();
}
private void updateDep(List<EmpSalaryItemEntity> enabled, List<EmpSalaryItemEntity> allItems) {
List<EmpSalarySysItemEntity> sysItems =
@@ -593,82 +637,6 @@ public class EmpSalaryItemService
repository.save(item3);
}
@SneakyThrows
public void calculate(
String organizationId, EmpSalaryBatchEntity batch, List<EmpSalaryEntity> list) {
if (list.isEmpty()) {
return;
}
long stdTime = 0;
long newInstanceTime = 0;
long setPropertyTime = 0;
long runTime = 0;
long x = System.nanoTime();
Class scriptClass = genClass(organizationId);
long y = System.nanoTime();
long genClassTime = y - x;
for (EmpSalaryEntity salary : list) {
long a = System.nanoTime();
Map<String, EmpSalaryStandardResult> stdMap =
empSalaryStandardService.calculateSalaryStandard(salary.getEmployee()).stream()
.collect(Collectors.toMap(e -> e.getName(), e -> e));
long b = System.nanoTime();
stdTime = stdTime + (b - a);
Script script = (Script) scriptClass.newInstance();
long c = System.nanoTime();
newInstanceTime = newInstanceTime + (c - b);
script.setProperty("salary", salary);
script.setProperty("emp", salary.getEmployee());
script.setProperty("stdMap", stdMap);
script.setProperty("batch", batch);
long d = System.nanoTime();
setPropertyTime = setPropertyTime + (d - c);
script.run();
long e = System.nanoTime();
runTime = runTime + (e - d);
}
long totalTime = stdTime + newInstanceTime + setPropertyTime + runTime + genClassTime;
log.info(
"totalTime: {} ns {} ms {}%\n"
+ "genClassTime: {} ns {} ms {}%\n"
+ "stdTime: {} ns {} ms {}%\n"
+ "newInstanceTime: {} ns {} ms {}%\n"
+ "setPropertyTime: {} ns {} ms {}%\n"
+ "runTime: {} ns {} ms {}%\n",
totalTime,
totalTime / 1000000,
1L * 100.0,
genClassTime,
genClassTime / 1000000,
genClassTime / totalTime * 100.0,
stdTime,
stdTime / 1000000,
stdTime / totalTime * 100.0,
newInstanceTime,
newInstanceTime / 1000000,
newInstanceTime / totalTime * 100.0,
setPropertyTime,
setPropertyTime / 1000000,
setPropertyTime / totalTime * 100.0,
runTime,
runTime / 1000000,
runTime / totalTime * 100.0);
}
public String genScript(String organizationId) {
List<EmpSalaryItemEntity> items =
@@ -759,8 +727,26 @@ public class EmpSalaryItemService
script.append("// 工资项目开始\n");
StringBuilder roundScript = new StringBuilder();
roundScript.append("// 保留小数位处理\n");
for (EmpSalaryItemEntity item : items) {
roundScript
.append("salary.")
.append(item.getCode())
.append(" = ")
.append(" ROUND( ")
.append(" salary.")
.append(item.getCode())
.append(" , ")
.append(ObjectUtils.defaultIfNull(item.getDecimalPlaces(), 2))
.append(" , ")
.append(StringUtils.defaultIfBlank(item.getDecimalPlacesHandler(), "0"))
.append(");\n");
if (StringUtils.equalsAnyIgnoreCase(item.getInputType(), "1")) {
script.append("/** \n")
@@ -793,6 +779,9 @@ public class EmpSalaryItemService
.append(" ?: 0")
.append(";\n");
continue;
}
@@ -827,56 +816,19 @@ public class EmpSalaryItemService
.append(" = (")
.append(item.getFormula())
.append(") ?: 0 ;\n");
}
script.append("// 工资项目结束\n");
roundScript.append("// 保留小数位处理\n");
script.append(roundScript);
return script.toString();
}
private Class genClass(String organizationId) {
List<EmpSalaryItemEntity> items =
this.findAll(
new Specification<EmpSalaryItemEntity>() {
@Override
public Predicate toPredicate(
Root<EmpSalaryItemEntity> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.and(
criteriaBuilder.equal(
root.get("organizationId"), organizationId),
criteriaBuilder.equal(root.get("status"), 1));
}
});
items.sort(Comparator.comparing(EmpSalaryItemEntity::getPriority));
var script = new StringBuilder();
for (EmpSalaryItemEntity item : items) {
if (StringUtils.equalsAnyIgnoreCase(item.getInputType(), "1")) {
continue;
}
if (StringUtils.isEmpty(item.getFormula())) {
throw new BizException("公式计算项公式不能为空: " + item.getName());
}
script.append("salary.")
.append(item.getCode())
.append(" = ")
.append(item.getFormula())
.append("\n");
}
Class scriptClass =
new GroovyClassLoader(this.getClass().getClassLoader(), new CompilerConfiguration())
.parseClass(script.toString());
return scriptClass;
}
public List<EmpSalaryItemEntity> getItems(String organizationId) {
return this.findAll(
@@ -892,4 +844,20 @@ public class EmpSalaryItemService
}
});
}
public String computeCacheKey(String organizationId) {
return this.repository.computeCacheKey(organizationId);
}
public Script newScriptInstance(String organizationId){
return groovyScriptManager.newInstance(computeCacheKey(organizationId), () -> genScript(organizationId));
}
}

View File

@@ -0,0 +1,141 @@
package cn.lihongjie.coal.employee.dto;
import cn.lihongjie.coal.base.dto.OrgCommonDto;
import cn.lihongjie.coal.common.DictCode;
import cn.lihongjie.coal.department.dto.DepartmentDto;
import cn.lihongjie.coal.jobPost.dto.JobPostDto;
import cn.lihongjie.coal.pojoProcessor.DictTranslate;
import jakarta.persistence.ManyToOne;
import lombok.Data;
import org.hibernate.annotations.Comment;
import java.time.LocalDate;
@Data
public class EmployeeCalculateDto extends OrgCommonDto {
@Comment("性别")
private String sex;
@Comment("性别-名称")
@DictTranslate(dictKey = DictCode.SEX)
private String sexName;
@Comment("民族")
private String nation;
@Comment("民族-名称")
@DictTranslate(dictKey = DictCode.NATION)
private String nationName;
@Comment("婚姻状况")
private String marriage;
@Comment("婚姻状况-名称")
@DictTranslate(dictKey = DictCode.MARRIAGE)
private String marriageName;
@Comment("入职时间")
private LocalDate entryDate;
@Comment("身份证号")
private String idCard;
@Comment("学历")
private String education;
@Comment("学历-名称")
@DictTranslate(dictKey = DictCode.EDUCATION)
private String educationName;
@Comment("毕业学校")
private String school;
@Comment("籍贯")
private String nativePlace;
@Comment("住址")
private String address;
@Comment("手机号")
private String phone;
@Comment("部门")
@ManyToOne
private DepartmentDto department;
@Comment("岗位")
@ManyToOne
private JobPostDto jobPost;
@Comment("银行编码")
private String bank;
@DictTranslate(dictKey = DictCode.BANK)
private String bankName;
@Comment("银行卡号")
private String bankCardNumber;
@Comment("收款人姓名")
private String bankCardName;
@Comment("离职时间")
private LocalDate resignDate;
@Comment("离职原因")
private String resignReason;
@Comment("员工状态")
private String empStatus;
@DictTranslate(dictKey = DictCode.EMP_STATUS)
private String empStatusName;
@Comment("养老保险基数")
private Double insurance1Base;
@Comment("养老保险比例")
private Double insurance1Percent;
@Comment("医疗保险基数")
private Double insurance2Base;
@Comment("医疗保险比例")
private Double insurance2Percent;
@Comment("失业保险基数")
private Double insurance3Base;
@Comment("失业保险比例")
private Double insurance3Percent;
@Comment("工伤保险基数")
private Double insurance4Base;
@Comment("工伤保险比例")
private Double insurance4Percent;
@Comment("生育保险基数")
private Double insurance5Base;
@Comment("生育保险比例")
private Double insurance5Percent;
@Comment("住房公积金基数")
private Double insurance6Base;
@Comment("住房公积金比例")
private Double insurance6Percent;
@Comment("工龄")
private Double workAge;
@Comment("年龄")
private Double age;
@Comment("出生日期")
private LocalDate birthday;
}

View File

@@ -2,6 +2,7 @@ package cn.lihongjie.coal.employee.mapper;
import cn.lihongjie.coal.base.mapper.BaseMapper;
import cn.lihongjie.coal.employee.dto.CreateEmployeeDto;
import cn.lihongjie.coal.employee.dto.EmployeeCalculateDto;
import cn.lihongjie.coal.employee.dto.EmployeeDto;
import cn.lihongjie.coal.employee.dto.UpdateEmployeeDto;
import cn.lihongjie.coal.employee.entity.EmployeeEntity;
@@ -17,4 +18,6 @@ public interface EmployeeMapper
extends BaseMapper<EmployeeEntity, EmployeeDto, CreateEmployeeDto, UpdateEmployeeDto> {
EmployeeCalculateDto toCalculateDto(EmployeeEntity list);
}

View File

@@ -10,10 +10,7 @@ import cn.lihongjie.coal.department.dto.DepartmentDto;
import cn.lihongjie.coal.department.entity.DepartmentEntity;
import cn.lihongjie.coal.department.service.DepartmentService;
import cn.lihongjie.coal.dictionary.service.DictionaryService;
import cn.lihongjie.coal.employee.dto.CreateEmployeeDto;
import cn.lihongjie.coal.employee.dto.EmployeeDto;
import cn.lihongjie.coal.employee.dto.ImportEmpFromExcelRequest;
import cn.lihongjie.coal.employee.dto.UpdateEmployeeDto;
import cn.lihongjie.coal.employee.dto.*;
import cn.lihongjie.coal.employee.entity.EmployeeEntity;
import cn.lihongjie.coal.employee.mapper.EmployeeMapper;
import cn.lihongjie.coal.employee.repository.EmployeeRepository;
@@ -267,6 +264,25 @@ public class EmployeeService extends BaseService<EmployeeEntity, EmployeeReposit
return collect;
}
public List<EmployeeCalculateDto> getCalculateDtoByIds(List<String> employeesIds) {
List<EmployeeEntity> entity = em.createQuery("select e from EmployeeEntity e left join fetch e.jobPost left join fetch e.department where e.id in :ids", EmployeeEntity.class)
.setParameter("ids", employeesIds)
.getResultList();
List<EmployeeCalculateDto> collect =
entity.stream()
.map(mapper::toCalculateDto)
.collect(Collectors.toList());
pojoProcessor.process(collect, false);
return collect;
}
public void refreshAge(String organizationId) {