package cn.fw.morax.service.biz.salary; import cn.fw.morax.common.utils.StringUtils; import cn.fw.morax.common.utils.ThreadPoolUtil; import cn.fw.morax.domain.bo.salary.SalaryGroupCalculableBO; import cn.fw.morax.domain.db.kpi.KpiPool; import cn.fw.morax.domain.db.salary.*; import cn.fw.morax.domain.enums.ExtraSalaryTypeEnum; import cn.fw.morax.domain.enums.SalaryCalMethodEnum; import cn.fw.morax.domain.enums.SalaryTypeEnum; import cn.fw.morax.rpc.ehr.EhrRpcService; import cn.fw.morax.service.biz.calculator.salary.SalaryBaseCalculator; import cn.fw.morax.service.biz.kpi.KpiPoolCommonService; import cn.fw.morax.service.data.salary.*; import cn.hutool.core.collection.ListUtil; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.YearMonth; import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; import java.util.*; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.stream.Collectors; import static cn.fw.common.businessvalidator.Validator.BV; /** * @author : kurisu * @version : 1.0 * @className : SalaryCalcService * @description : 薪资计算类 * @date : 2022-04-25 16:24 */ @Slf4j @Service public class SalaryCalcService { private final SalaryGeneralSettinService salaryGeneralSettinService; private final SalaryGroupService salaryGroupService; private final SalaryGroupUserService salaryGroupUserService; private final SalaryPoolService salaryPoolService; private final SalaryGroupProjectService salaryGroupProjectService; private final StringRedisTemplate stringRedisTemplate; private final EhrRpcService ehrRpcService; private final SalaryPoolDetailService salaryPoolDetailService; private final SalaryPoolCommonService salaryPoolCommonService; private final KpiPoolCommonService kpiPoolCommonService; private final SalaryConfirmBizService salaryConfirmBizService; private static final List EXTRA_SALARY_TYPE_ENUM_LIST = ListUtil.toList(ExtraSalaryTypeEnum.AWARD, ExtraSalaryTypeEnum.PENALTY, ExtraSalaryTypeEnum.NECESSARY, ExtraSalaryTypeEnum.SUBSIDY, ExtraSalaryTypeEnum.PERSON_TAX); @Value("${spring.cache.custom.global-prefix}:calculable:salary-group") @Getter private String calculableSalaryKey; /** * 绩效变动导致重新计算工资的缓存key */ @Value("${spring.cache.custom.global-prefix}:salary-change:salary-recalculate") @Getter private String retrySalaryCalcKey; private final Map calculatorMap; @Autowired public SalaryCalcService(final SalaryGeneralSettinService salaryGeneralSettinService, final SalaryGroupService salaryGroupService, final SalaryGroupUserService salaryGroupUserService, final SalaryPoolService salaryPoolService, final SalaryPoolDetailService salaryPoolDetailService, final SalaryGroupProjectService salaryGroupProjectService, final SalaryPoolCommonService salaryPoolCommonService, final KpiPoolCommonService kpiPoolCommonService, final StringRedisTemplate stringRedisTemplate, EhrRpcService ehrRpcService, final SalaryConfirmBizService salaryConfirmBizService, final List salaryCalculatorList) { this.salaryGeneralSettinService = salaryGeneralSettinService; this.salaryGroupService = salaryGroupService; this.salaryGroupUserService = salaryGroupUserService; this.salaryPoolService = salaryPoolService; this.salaryPoolDetailService = salaryPoolDetailService; this.salaryGroupProjectService = salaryGroupProjectService; this.salaryPoolCommonService = salaryPoolCommonService; this.kpiPoolCommonService = kpiPoolCommonService; this.stringRedisTemplate = stringRedisTemplate; this.ehrRpcService = ehrRpcService; this.salaryConfirmBizService = salaryConfirmBizService; this.calculatorMap = salaryCalculatorList.stream().collect(Collectors.toMap(SalaryBaseCalculator::getCalMethod, v -> v)); } /** * 准备计算薪酬 * * @param localDate */ public void prepareCalcSalary(final LocalDate localDate) { Set ids = salaryGroupService.querySalaryGroupIdByDay(localDate); if (CollectionUtils.isEmpty(ids)) { return; } String[] array = ids.stream() .map(salaryGroupId -> new SalaryGroupCalculableBO(salaryGroupId, localDate, Boolean.TRUE)) .map(JSONObject::toJSONString) .toArray(String[]::new); stringRedisTemplate.opsForSet().add(getCalculableSalaryKey(), array); } /** * 计算薪资 */ public void calculateSalary() { BoundSetOperations setOps = stringRedisTemplate.boundSetOps(getCalculableSalaryKey()); ThreadPoolExecutor threadPool = ThreadPoolUtil.getInstance().getThreadPool(); List overflowsList = new ArrayList<>(); String str; while ((str = setOps.pop()) != null) { final SalaryGroupCalculableBO bo = JSONObject.parseObject(str, SalaryGroupCalculableBO.class); if (Objects.isNull(bo)) { continue; } try { String finalStr = str; threadPool.execute(() -> { try { calculateSalary(bo); } catch (Exception e) { log.error("计算薪资数据失败:{}", bo, e); setOps.add(finalStr); } }); } catch (RejectedExecutionException re) { overflowsList.add(str); } } if (!CollectionUtils.isEmpty(overflowsList)) { for (String s : overflowsList) { setOps.add(s); } } } /** * 计算薪资数据 * * @param bo */ public void calculateSalary(SalaryGroupCalculableBO bo) { final Long salaryGroupId = bo.getSalaryGroupId(); final LocalDate localDate = bo.getLocalDate(); final boolean calcTotal = Boolean.TRUE.equals(bo.getCalcTotal()); SalaryGroup salaryGroup = salaryGroupService.getById(salaryGroupId); if (Objects.isNull(salaryGroup)) { log.error("薪酬组[{}]不存在", salaryGroupId); return; } if (!Boolean.TRUE.equals(salaryGroup.getYn())) { log.error("无法计算被删除的薪酬组数据: [{}]", salaryGroup.getId()); return; } List userList = salaryGroupUserService.list(Wrappers.lambdaQuery() .eq(SalaryGroupUser::getSalaryGroupId, salaryGroupId) .eq(SalaryGroupUser::getDataDate, localDate) .eq(SalaryGroupUser::getYn, Boolean.TRUE) ); if (CollectionUtils.isEmpty(userList)) { log.error("薪酬组[{}]不包含任何员工", salaryGroupId); return; } for (SalaryGroupUser salaryUser : userList) { calcUserSalary(salaryUser, calcTotal); } } /** * 绩效变动导致的用户薪酬重新计算准备 */ public void retryCalcSalary() { final String salaryCalcKey = getRetrySalaryCalcKey(); BoundSetOperations setOps = stringRedisTemplate.boundSetOps(salaryCalcKey); List failList = new ArrayList<>(); String str; while ((str = setOps.pop()) != null) { try { if (StringUtils.isNumber(str)) { retryCalcUserSalary(Long.valueOf(str)); } } catch (Exception ex) { log.error("重新计算薪资数据失败:[{}]", str, ex); failList.add(str); } } if (!CollectionUtils.isEmpty(failList)) { for (String s : failList) { setOps.add(s); } } } /** * 手动 计算人员薪资 * * @param salaryGroupUserId * @param calcTotal */ public void calcUserSalary(Long salaryGroupUserId, boolean calcTotal) { SalaryGroupUser user = salaryGroupUserService.getById(salaryGroupUserId); if (Objects.isNull(user) || !Boolean.TRUE.equals(user.getYn())) { return; } calcUserSalary(user, calcTotal); } /** * 申述后重新计算人员薪资 * * @param salaryGroupUserId */ public void retryCalcUserSalary(Long salaryGroupUserId) { SalaryGroupUser user = salaryGroupUserService.getById(salaryGroupUserId); if (Objects.isNull(user) || !Boolean.TRUE.equals(user.getYn())) { return; } final Long groupId = user.getGroupId(); SalaryGeneralSettin settin = salaryGeneralSettinService.getOne(Wrappers.lambdaQuery() .eq(SalaryGeneralSettin::getYn, Boolean.TRUE) .eq(SalaryGeneralSettin::getGroupId, groupId) , Boolean.FALSE); if (Objects.isNull(settin)) { settin = salaryGeneralSettinService.initData(groupId); } String datesOfAppeal = Optional.ofNullable(settin.getDatesOfAppeal()).orElse("3,5"); int payoffDay = Optional.ofNullable(settin.getPayoffDate()).orElse(15); int dayOfMonth = LocalDate.now().getDayOfMonth(); if (dayOfMonth >= payoffDay) { log.error("绩效组人员[{}]_已经超过工资发放时间,无法重新计算", user.getId()); return; } String[] daysArr = datesOfAppeal.split(","); int startDay = Integer.parseInt(daysArr[0]); int endDay = Integer.parseInt(daysArr[1]); if (dayOfMonth < startDay || dayOfMonth > endDay) { log.error("绩效组人员[{}]_已经超过申述时间,无法重新计算", user.getId()); return; } SalaryPool pool = salaryPoolCommonService.inspectionPool(user); if (Boolean.TRUE.equals(pool.getPaid())) { log.error("绩效组人员[{}] 工资已经发放,无法计算", user.getId()); return; } calcUserSalary(pool, user, true); salaryConfirmBizService.retryCreateSalaryConfirmTodo(pool, user, payoffDay); } /** * 计算用户薪资 * * @param user */ public void calcUserSalary(SalaryGroupUser user, boolean calcTotal) { SalaryPool pool = salaryPoolCommonService.inspectionPool(user); if (Boolean.TRUE.equals(pool.getRegular()) || Boolean.TRUE.equals(pool.getPaid())) { log.error("绩效组人员[{}] 已确认工资明细或工资已经发放,无法计算", user.getId()); return; } calcUserSalary(pool, user, calcTotal); } /** * 计算用户薪资 * * @param user */ @Transactional(rollbackFor = Exception.class) public void calcUserSalary(SalaryPool pool, SalaryGroupUser user, boolean calcTotal) { final Long salaryGroupId = user.getSalaryGroupId(); final LocalDate dataDate = user.getDataDate(); List salaryProjectList = salaryGroupProjectService.list(Wrappers.lambdaQuery() .eq(SalaryGroupProject::getSalaryGroupId, salaryGroupId) .eq(SalaryGroupProject::getYn, Boolean.TRUE) ); if (CollectionUtils.isEmpty(salaryProjectList)) { log.error("薪酬组[{}]薪酬项未配置", salaryGroupId); return; } List detailList = new ArrayList<>(); if (user.getProbationer()) { calcProbationSalary(pool, detailList, user); } else { calcPushMoney(salaryProjectList, user, pool, detailList); } calcExtraMoney(pool, dataDate, detailList); saveSalaryPoolDetails(detailList, pool.getId(), dataDate); if (calcTotal) { calcTotal(pool, detailList, user); } } /** * 计算总金额 * * @param pool * @param detailList */ private void calcTotal(SalaryPool pool, List detailList, SalaryGroupUser user) { if (CollectionUtils.isEmpty(detailList)) { return; } if (!user.getProbationer()) { KpiPool kpiPool = kpiPoolCommonService.queryPool(pool); if (Objects.nonNull(kpiPool)) { pool.setStarLevel(kpiPool.getActualStar()); } } BigDecimal total = BigDecimal.ZERO; for (SalaryPoolDetail detail : detailList) { BigDecimal actualSalaryAmount = detail.getActualSalaryAmount(); SalaryTypeEnum salaryType = detail.getType(); total = salaryType.isPlus() ? total.add(actualSalaryAmount) : total.subtract(actualSalaryAmount); } pool.setReward(total); pool.setDataDate(user.getDataDate()); salaryPoolService.updateById(pool); } /** * 保存薪资明细 * * @param detailList */ private void saveSalaryPoolDetails(List detailList, Long poolId, LocalDate date) { if (CollectionUtils.isEmpty(detailList)) { return; } salaryPoolDetailService.remove(Wrappers.lambdaQuery() .eq(SalaryPoolDetail::getSalaryPoolId, poolId) .eq(SalaryPoolDetail::getSalaryDate, date) ); salaryPoolDetailService.saveBatch(detailList); } /** * 计算绩效金额 * * @param salaryProjectList * @param user * @param pool * @param detailList */ private void calcPushMoney(List salaryProjectList, SalaryGroupUser user, SalaryPool pool, List detailList) { for (SalaryGroupProject salaryGroupProject : salaryProjectList) { final String salaryProjectName = salaryGroupProject.getName(); SalaryBaseCalculator calculator = calculatorMap.get(salaryGroupProject.getCalMethod()); if (Objects.isNull(calculator)) { log.error("[{}_{}]计算器不存在", salaryGroupProject.getId(), salaryProjectName); return; } final BigDecimal salaryMoney = calculator.calculate(salaryGroupProject, user); if (Objects.nonNull(salaryMoney)) { SalaryPoolDetail detail = createDetail(pool, user.getDataDate()); detail.setSalaryGroupProjectId(salaryGroupProject.getId()); detail.setSalaryGroupProjectName(salaryProjectName); detail.setSalaryAmount(salaryMoney); detail.setActualSalaryAmount(salaryMoney); detailList.add(detail); } } } /** * 计算额外金额 * * @param pool * @param date * @param detailList */ private void calcExtraMoney(SalaryPool pool, LocalDate date, List detailList) { SalaryBaseCalculator calculator = calculatorMap.get(SalaryCalMethodEnum.DYNAMIC); BV.notNull(calculator, () -> "动态金额计算器不存在"); for (ExtraSalaryTypeEnum extraSalaryTypeEnum : EXTRA_SALARY_TYPE_ENUM_LIST) { SalaryPoolDetail salaryPoolDetail = calculator.calcDynamicMoney(pool, date, extraSalaryTypeEnum); detailList.add(salaryPoolDetail); } } /** * 计算试用期工资 * * @param pool * @param detailList * @param user */ private void calcProbationSalary(SalaryPool pool, List detailList, SalaryGroupUser user) { int count = salaryGroupUserService.count(Wrappers.lambdaQuery() .eq(SalaryGroupUser::getUserId, user.getUserId()) .eq(SalaryGroupUser::getPostId, user.getPostId()) .eq(SalaryGroupUser::getShopId, user.getShopId()) .eq(SalaryGroupUser::getProbationer, Boolean.TRUE) .eq(SalaryGroupUser::getFrozen, Boolean.FALSE) .eq(SalaryGroupUser::getYn, Boolean.TRUE) .ge(SalaryGroupUser::getDataDate, user.getDataDate().with(TemporalAdjusters.firstDayOfMonth())) .le(SalaryGroupUser::getDataDate, user.getDataDate()) ); if (count <= 0) { return; } int lengthOfMonth = YearMonth.from(user.getDataDate()).lengthOfMonth(); BigDecimal dayP = new BigDecimal(count).divide(new BigDecimal(lengthOfMonth), 2, RoundingMode.HALF_UP); BigDecimal baseValue = ehrRpcService.queryProbationerSalary(user.getUserId(), user.getDataDate()); BV.notNull(baseValue, () -> "试用期工资查询失败"); BigDecimal probationSalary = baseValue.multiply(dayP).divide(BigDecimal.ONE, 2, RoundingMode.HALF_UP); SalaryPoolDetail detail = createDetail(pool, user.getDataDate()); detail.setType(SalaryTypeEnum.PROBATION); detail.setSalaryAmount(probationSalary); detail.setActualSalaryAmount(probationSalary); detailList.add(detail); } /** * 创建实体 * * @param pool * @param date * @return */ private SalaryPoolDetail createDetail(SalaryPool pool, LocalDate date) { SalaryPoolDetail poolDetail = new SalaryPoolDetail(); poolDetail.setSalaryPoolId(pool.getId()); poolDetail.setType(SalaryTypeEnum.ROYALTIES); poolDetail.setProcessedAmount(BigDecimal.ZERO); poolDetail.setSalaryAmount(BigDecimal.ZERO); poolDetail.setActualSalaryAmount(BigDecimal.ZERO); poolDetail.setSalaryDate(date); poolDetail.setGroupId(pool.getGroupId()); poolDetail.setYn(Boolean.TRUE); return poolDetail; } }