package cn.fw.morax.service.biz.kpi; import cn.fw.common.cache.CacheExtender; import cn.fw.common.cache.locker.DistributedLocker; import cn.fw.morax.common.utils.MessageFormatUtil; import cn.fw.morax.common.utils.PublicUtil; import cn.fw.morax.domain.db.UserResignExamine; import cn.fw.morax.domain.db.kpi.KpiGroupUser; import cn.fw.morax.domain.db.kpi.KpiPool; import cn.fw.morax.domain.db.kpi.KpiStarRule; import cn.fw.morax.domain.db.salary.SalaryGroupUser; import cn.fw.morax.domain.db.salary.SalaryPool; import cn.fw.morax.domain.db.salary.SalaryClosure; import cn.fw.morax.domain.enums.ResignExamineStatusEnum; import cn.fw.morax.domain.enums.StarLevelEnum; import cn.fw.morax.service.biz.salary.SalarySettingCommonService; import cn.fw.morax.service.data.UserResignExamineService; import cn.fw.morax.service.data.kpi.KpiPoolService; import cn.fw.morax.service.data.kpi.KpiStarRuleService; import cn.fw.morax.service.data.salary.SalaryGeneralSettinService; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.locks.Lock; import java.util.stream.Collectors; import static cn.fw.common.businessvalidator.Validator.BV; /** * @author : kurisu * @version : 1.0 * @className : KpiPoolCommonService * @description : 绩效池公用方法 * @date : 2022-09-27 11:40 */ @Slf4j @Service @RequiredArgsConstructor public class KpiPoolCommonService { public static final CacheKeyFunction KPI_GROUP_AVERAGE_RATIO_CACHE_NAME = (month, groupId) -> String.format("kpi-group:average-ratio:%s:%s", month.toString(), groupId); private final KpiPoolService kpiPoolService; private final DistributedLocker distributedLocker; private final SalaryGeneralSettinService salaryGeneralSettinService; private final UserResignExamineService userResignExamineService; private final KpiStarRuleService kpiStarRuleService; private final CacheExtender cacheExtender; private final SalarySettingCommonService salarySettingCommonService; @Value("${spring.cache.custom.global-prefix}:forced:resign") @Getter private String forcedResignLockKey; /** * 查询绩效池 * * @param user * @return */ public Optional queryPool(SalaryGroupUser user) { KpiPool kpiPool = kpiPoolService.getOne(Wrappers.lambdaQuery() .eq(KpiPool::getUserId, user.getUserId()) .eq(KpiPool::getPostId, user.getPostId()) .eq(KpiPool::getShopId, user.getShopId()) .eq(KpiPool::getMonthly, YearMonth.from(user.getDataDate())) .eq(KpiPool::getYn, Boolean.TRUE) , Boolean.FALSE); if (PublicUtil.isEmpty(kpiPool)) { log.error("绩效数据不存在,用户:{}", JSON.toJSONString(user)); // throw new BusinessException("用户[" + user.getUserId() + "]的绩效数据不存在"); } return Optional.ofNullable(kpiPool); } /** * 查询绩效池 * * @param pool * @return */ public KpiPool queryPool(SalaryPool pool) { KpiPool kpiPool = kpiPoolService.getOne(Wrappers.lambdaQuery() .eq(KpiPool::getUserId, pool.getUserId()) .eq(KpiPool::getPostId, pool.getPostId()) .eq(KpiPool::getShopId, pool.getShopId()) .eq(KpiPool::getMonthly, pool.getMonthly()) .eq(KpiPool::getYn, Boolean.TRUE) , Boolean.FALSE); if (PublicUtil.isEmpty(kpiPool)) { log.error("绩效数据不存在,薪酬池:{}", JSON.toJSONString(pool)); // throw new BusinessException("用户[" + pool.getUserId() + "]的绩效数据不存在"); } return kpiPool; } /** * 查询绩效池(上报绩效、计算绩效得分用) * * @param user * @return */ public KpiPool inspectionPool(KpiGroupUser user) { KpiPool kpiPool = kpiPoolService.getOne(Wrappers.lambdaQuery() .eq(KpiPool::getKpiGroupId, user.getKpiGroupId()) .eq(KpiPool::getUserId, user.getUserId()) .eq(KpiPool::getMonthly, YearMonth.from(user.getDataDate())) .eq(KpiPool::getYn, Boolean.TRUE) , Boolean.FALSE); if (Objects.isNull(kpiPool)) { kpiPool = kpiPoolService.getOne(Wrappers.lambdaQuery() .eq(KpiPool::getPostId, user.getPostId()) .eq(KpiPool::getShopId, user.getShopId()) .eq(KpiPool::getUserId, user.getUserId()) .eq(KpiPool::getMonthly, YearMonth.from(user.getDataDate())) .eq(KpiPool::getYn, Boolean.TRUE) , Boolean.FALSE); if (Objects.isNull(kpiPool)) { kpiPool = createPoolData(user); } else { kpiPool = this.modifyPoolData(kpiPool, user); } } else if ((!kpiPool.getShopId().equals(user.getShopId())) || (!kpiPool.getKpiGroupId().equals(user.getKpiGroupId()))) { //若门店信息与员工在职门店不匹配,说明员工是调门店,绩效组没变 //若绩效组id不匹配,说明绩效组编辑生成了新绩效组,对应绩效池中的绩效组id修改 kpiPool = this.modifyPoolData(kpiPool, user); } return kpiPool; } /** * 固定绩效数据 * * @param groupId */ public void regularKpiStar(Long groupId) { String key = String.format("%s:%s", ":regular:kpi-start:group:", groupId); Lock lock = distributedLocker.lock(key); if (!((RLock) lock).isLocked()) { return; } try { LocalDate current = LocalDate.now(); List salaryClosures = salarySettingCommonService.queryNoClosureKpis(groupId); for (SalaryClosure salaryClosure : salaryClosures) { //当前时间在申述时间之后 if (current.compareTo(salaryClosure.getAppealEndTime()) > 0) { regularStarAndExamineUser(groupId, YearMonth.now().minusMonths(1L)); salarySettingCommonService.updateClosureKpi(salaryClosure); } } } finally { lock.unlock(); } } @Transactional(rollbackFor = Exception.class) public void forcedResign() { Lock lock = distributedLocker.lock(getForcedResignLockKey()); if (!((RLock) lock).isLocked()) { return; } try { List list = userResignExamineService.list(Wrappers.lambdaQuery() .eq(UserResignExamine::getStatus, ResignExamineStatusEnum.WAITING) ); if (CollectionUtils.isEmpty(list)) { return; } for (UserResignExamine examine : list) { examine.setStatus(ResignExamineStatusEnum.COMPLETE); // todo 对接人事员工离职接口 } userResignExamineService.updateBatchById(list); } finally { lock.unlock(); } } @Transactional(rollbackFor = Exception.class) public KpiPool modifyPoolData(KpiPool pool, KpiGroupUser user) { pool.setKpiGroupId(user.getKpiGroupId()); pool.setShopId(user.getShopId()); pool.setShopName(user.getShopName()); pool.setKgc(user.getKgc()); kpiPoolService.updateById(pool); return pool; } @Transactional(rollbackFor = Exception.class) public KpiPool createPoolData(KpiGroupUser groupUser) { KpiPool pool = new KpiPool(); pool.setKpiGroupId(groupUser.getKpiGroupId()); pool.setKgc(groupUser.getKgc()); pool.setUserId(groupUser.getUserId()); pool.setUserName(groupUser.getUserName()); pool.setPostId(groupUser.getPostId()); pool.setPostName(groupUser.getPostName()); pool.setShopId(groupUser.getShopId()); pool.setShopName(groupUser.getShopName()); pool.setStarLevel(StarLevelEnum.C); pool.setActualStar(StarLevelEnum.C); pool.setRevoked(Boolean.FALSE); pool.setInclusion(!Boolean.TRUE.equals(groupUser.getIgnored())); pool.setRegular(Boolean.FALSE); pool.setMonthly(YearMonth.from(groupUser.getDataDate())); pool.setGroupId(groupUser.getGroupId()); pool.setYn(Boolean.TRUE); pool.setKpiScore(BigDecimal.ZERO); pool.setKpiScoreRatio(BigDecimal.ZERO); pool.setAverageKpiScoreRatio(BigDecimal.ZERO); kpiPoolService.save(pool); return pool; } /** * 固定星级 * * @param groupId * @param yearMonth */ @Transactional(rollbackFor = Exception.class) public void regularStarAndExamineUser(Long groupId, YearMonth yearMonth) { List list = kpiPoolService.list(Wrappers.lambdaQuery() .eq(KpiPool::getGroupId, groupId) .eq(KpiPool::getMonthly, yearMonth) .eq(KpiPool::getYn, Boolean.TRUE) .ne(KpiPool::getRegular, Boolean.TRUE) .last(" limit 2000 ") ); if (CollectionUtils.isEmpty(list)) { return; } final KpiStarRule starRule = kpiStarRuleService.getOne(Wrappers.lambdaQuery() .eq(KpiStarRule::getGroupId, groupId) .eq(KpiStarRule::getYn, Boolean.TRUE) , Boolean.FALSE); BV.notNull(starRule, () -> MessageFormatUtil.MessageFormatTransfer("集团[{0}]为配置星级规则", groupId)); String cacheName = KPI_GROUP_AVERAGE_RATIO_CACHE_NAME.apply(yearMonth, groupId); for (KpiPool kpiPool : list) { kpiPool.setRegular(Boolean.TRUE); examineUser(kpiPool, starRule); cacheExtender.evictCache(cacheName, kpiPool.getKpiGroupId()); } kpiPoolService.updateBatchById(list); } /** * 校验用户是否触发离职 * * @param kpiPool * @param starRule */ private void examineUser(KpiPool kpiPool, KpiStarRule starRule) { if (!Boolean.TRUE.equals(kpiPool.getInclusion())) { return; } int maxUnqualifiedTimes = Optional.ofNullable(starRule.getMaxUnqualifiedTimes()).orElse(12); int forcedTurnoverCycle = Optional.ofNullable(starRule.getForcedTurnoverCycle()).orElse(3); Long userId = kpiPool.getUserId(); List list = kpiPoolService.list(Wrappers.lambdaQuery() .eq(KpiPool::getGroupId, kpiPool.getGroupId()) .eq(KpiPool::getUserId, userId) .eq(KpiPool::getInclusion, Boolean.TRUE) .eq(KpiPool::getYn, Boolean.TRUE) .eq(KpiPool::getRegular, Boolean.TRUE) .lt(KpiPool::getMonthly, kpiPool.getMonthly()) .orderByDesc(KpiPool::getMonthly) .last(" limit " + (maxUnqualifiedTimes - 1)) ); if (CollectionUtils.isEmpty(list)) { return; } list.add(kpiPool); revokedKpiTimes(list, starRule); forcedTurnover(list, forcedTurnoverCycle); } /** * 减少D级判断 * * @param list * @param starRule */ private void revokedKpiTimes(List list, KpiStarRule starRule) { int continuousMonthly = Optional.ofNullable(starRule.getContinuousMonthly()).orElse(2); BigDecimal minScoreRatio = Optional.ofNullable(starRule.getMinScoreRatio()).orElse(BigDecimal.valueOf(0.5)); if (list.size() == 0 || list.size() < continuousMonthly) { return; } KpiPool current = list.get(list.size() - 1); if (Objects.isNull(current)) { return; } BigDecimal averageRatio = kpiGroupAverageRatio(current.getKpiGroupId(), current.getMonthly(), current.getGroupId()); if (BigDecimal.ZERO.compareTo(averageRatio) >= 0) { return; } BigDecimal kpiScoreRatio = current.getKpiScoreRatio(); if (averageRatio.compareTo(kpiScoreRatio) >= 0) { return; } boolean revokeAble = Boolean.TRUE; for (int i = 1; i <= continuousMonthly; i++) { KpiPool pool = list.get(list.size() - i); BigDecimal scoreRatio = pool.getKpiScoreRatio(); revokeAble = revokeAble && minScoreRatio.compareTo(scoreRatio) < 0; } if (!revokeAble) { return; } for (KpiPool pool : list) { if (StarLevelEnum.D.equals(pool.getActualStar()) && !Boolean.TRUE.equals(pool.getRevoked())) { pool.setRevoked(Boolean.TRUE); kpiPoolService.updateById(pool); break; } } } /** * 强制离职判断 * * @param list * @param forcedTurnoverCycle */ private void forcedTurnover(List list, int forcedTurnoverCycle) { if (CollectionUtils.isEmpty(list)) { return; } int count = (int) list.stream() .filter(r -> StarLevelEnum.D.equals(r.getActualStar())) .filter(p -> !Boolean.TRUE.equals(p.getRevoked())) .count(); if (forcedTurnoverCycle > count) { return; } KpiPool pool = list.get(list.size() - 1); UserResignExamine examine = new UserResignExamine(); examine.setUserId(pool.getUserId()); examine.setUserName(pool.getUserName()); examine.setShopId(pool.getShopId()); examine.setMonthly(pool.getMonthly()); // examine.setStatus(ResignExamineStatusEnum.IGNORE); fixme 等稳定了使用这行代码 examine.setStatus(ResignExamineStatusEnum.IGNORE); userResignExamineService.save(examine); } /** * calculate kpi group's specified month average score ratio * * @param kpiGroupId * @param yearMonth * @param groupId * @return average score ratio */ private BigDecimal kpiGroupAverageRatio(Long kpiGroupId, YearMonth yearMonth, Long groupId) { String cacheName = KPI_GROUP_AVERAGE_RATIO_CACHE_NAME.apply(yearMonth, groupId); BigDecimal averageRatio = cacheExtender.getCache(cacheName, kpiGroupId, BigDecimal.class); if (Objects.nonNull(averageRatio)) { return averageRatio; } List list = kpiPoolService.list(Wrappers.lambdaQuery() .eq(KpiPool::getGroupId, groupId) .eq(KpiPool::getKpiGroupId, kpiGroupId) .eq(KpiPool::getInclusion, Boolean.TRUE) .eq(KpiPool::getYn, Boolean.TRUE) .eq(KpiPool::getMonthly, yearMonth) ); if (CollectionUtils.isEmpty(list)) { return BigDecimal.ZERO; } Double averaging = list.stream().map(KpiPool::getKpiScoreRatio).filter(Objects::nonNull).collect(Collectors.averagingDouble(BigDecimal::doubleValue)); averageRatio = BigDecimal.valueOf(averaging); cacheExtender.putCache(cacheName, kpiGroupId, averageRatio); return averageRatio; } @FunctionalInterface interface CacheKeyFunction { /** * Applies this function to the given arguments. * * @param t the function argument1 * @param u the function argument2 * @return the function result */ R apply(T t, U u); } }