package com.punchsettle.server.service.manager.impl; import java.sql.Timestamp; import java.time.DayOfWeek; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import com.punchsettle.server.common.exception.BusinessException; import com.punchsettle.server.common.utils.Assert; import com.punchsettle.server.constant.PunchInCategoryEnum; import com.punchsettle.server.constant.PunchInRuleEnum; import com.punchsettle.server.constant.PunchInSettleTypeEnum; import com.punchsettle.server.constant.PunchInStatusEnum; import com.punchsettle.server.dto.settle.SettleDto; import com.punchsettle.server.dto.settle.SettleInfoDto; import com.punchsettle.server.dto.settle.SettleQuery; import com.punchsettle.server.dto.settle.SettleRequest; import com.punchsettle.server.dto.settle.SettleResultDto; import com.punchsettle.server.utiis.SpringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import com.punchsettle.server.atomic.entity.PunchIn; import com.punchsettle.server.atomic.entity.PunchInRecord; import com.punchsettle.server.atomic.entity.PunchInRecordSettlementRela; import com.punchsettle.server.atomic.entity.PunchInSettlement; import com.punchsettle.server.atomic.entity.SettlementTask; import com.punchsettle.server.atomic.entity.User; import com.punchsettle.server.atomic.service.IPunchInRecordService; import com.punchsettle.server.atomic.service.IPunchInRecordSettlementRelaService; import com.punchsettle.server.atomic.service.IPunchInService; import com.punchsettle.server.atomic.service.IPunchInSettlementService; import com.punchsettle.server.atomic.service.ISettlementTaskService; import com.punchsettle.server.atomic.service.IUserService; import com.punchsettle.server.dto.punchin.PunchInQuery; import com.punchsettle.server.dto.punchin.PunchInRecordQuery; import com.punchsettle.server.dto.task.SettleRewardTaskDto; import com.punchsettle.server.service.manager.ISettleManager; import com.punchsettle.server.utiis.DateUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; /** * @author tyuio * @version 1.0.0 * @description 结算服务类 * @date 2024/12/12 22:29 */ @Slf4j @Service public class SettleManagerImpl implements ISettleManager { @Autowired private IPunchInService punchInService; @Autowired private IPunchInRecordService punchInRecordService; @Autowired private IUserService userService; @Autowired private IPunchInSettlementService punchInSettlementService; @Autowired private ISettlementTaskService settlementTaskService; @Autowired private IPunchInRecordSettlementRelaService punchInRecordSettlementRelaService; @Override @Transactional(rollbackFor = Exception.class) public void settleHandler(PunchInSettleTypeEnum settleType, LocalDate settleDate, List userIds, List punchInIds) { if (Objects.isNull(settleType)) { log.info("结算任务异常,原因:没有指定结算类型"); return; } if (Objects.isNull(settleDate)) { log.info("结算任务异常,原因:没有指定结算日期"); return; } if (settleDate.isAfter(LocalDate.now())) { BusinessException.throwFail(String.format("结算任务异常,原因:结算日期{} 不能大于等于今天", settleDate)); } log.info("结算任务开始,结算类型:{}, 结算日期:{}", settleType.getName(), settleDate); Timestamp settleStartTime = new Timestamp(System.currentTimeMillis()); SettleInfoDto settleInfo = new SettleInfoDto(settleType, settleDate); // 读取用户数据,如果userIds为空则结算所有的用户 userIds = Optional.ofNullable(userIds).orElseGet(() -> { PunchInRecordQuery recordQuery = new PunchInRecordQuery(); recordQuery.setStartDate(settleInfo.getSettleDateStr()); recordQuery.setEndDate(settleInfo.getSettleDateStr()); List punchInRecords = punchInRecordService.listByCondition(recordQuery); return punchInRecords.stream().map(PunchInRecord::getCreatedBy).collect(Collectors.toList()); }); List users = userService.listByIds(userIds); if (CollectionUtils.isEmpty(users)) { log.info("结算任务结束,原因:没有找到待结算的用户信息"); return; } // 读取用户的打卡任务,如果punchIds为空则结算用户所有的打卡任务 PunchInQuery punchInQuery = new PunchInQuery(); punchInQuery.setUserIds(userIds); punchInQuery.setPunchInIds(punchInIds); List punchIns = punchInService.listByCondition(punchInQuery); if (CollectionUtils.isEmpty(punchIns)) { log.info("结算任务结束,原因:没有找到打卡任务"); return; } // 获取最新的打卡任务ID punchInIds = punchIns.stream().map(PunchIn::getId).collect(Collectors.toList()); // 获取结算日的打卡记录 PunchInRecordQuery recordQuery = new PunchInRecordQuery(); recordQuery.setStartDate(settleInfo.getSettleDateStr()); recordQuery.setEndDate(settleInfo.getSettleDateStr()); recordQuery.setPunchInIds(punchInIds); List punchInRecords = punchInRecordService.listByCondition(recordQuery); if (CollectionUtils.isEmpty(punchInRecords)) { log.info("结算任务结束,原因:没有打卡记录"); return; } // 获取一周的打卡记录 Map> weeklyPunchInRecords = Map.of(); if (settleInfo.getSundayFlag() || PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType())) { PunchInRecordQuery weeklyRecordQuery = new PunchInRecordQuery(); weeklyRecordQuery.setPunchInIds(punchInIds); weeklyRecordQuery.setStartDate(DateUtils.getLastWeekMonday(settleInfo.getSettleDate()).toString()); weeklyRecordQuery.setEndDate(DateUtils.getLastWeekSunday(settleInfo.getSettleDate()).toString()); List punchInRecordForWeeks = punchInRecordService.listByCondition(weeklyRecordQuery); if (!CollectionUtils.isEmpty(punchInRecordForWeeks)) { weeklyPunchInRecords = punchInRecordForWeeks.stream().collect(Collectors.groupingBy(PunchInRecord::getPunchInId)); } } // 用户-打卡任务分组 Map> userPunchInMap = punchIns.stream().collect(Collectors.groupingBy(PunchIn::getCreatedBy)); // 打卡任务-打卡记录 map Map punchInRecordMap = punchInRecords.stream().collect(Collectors.toMap(PunchInRecord::getPunchInId, Function.identity(), (key1, key2) -> key1)); // 待更新的用户 List updateUsers = new ArrayList<>(); // 新增的打卡结算关联关系 List addRelas = new ArrayList<>(); // 待更新的打卡记录 List updatePunchInRecords = new ArrayList<>(); // 待新增的打卡记录 List addPunchInRecords = new ArrayList<>(); // 待新增的打卡结算信息 List addPunchInSettlements = new ArrayList<>(); // 先创建结算任务执行记录 SettlementTask settlementTask = new SettlementTask(); settlementTask.setSettleDate(settleDate.toString()); settlementTask.setStartTime(settleStartTime); settlementTask.setProcessedNum(users.size()); settlementTaskService.insert(settlementTask); // 结算 for (User user : users) { SettleResultDto settleResult = settle(settleInfo, user, userPunchInMap.get(user.getId()), punchInRecordMap, weeklyPunchInRecords); updateUsers.add(settleResult.getUpdateUser()); addRelas.addAll(settleResult.getAddRelas()); addPunchInRecords.addAll(settleResult.getAddPunchInRecords()); updatePunchInRecords.addAll(settleResult.getUpdatePunchInRecords()); addPunchInSettlements.add(settleResult.getAddPunchInSettlements()); } // 更新用户奖励信息 if (!CollectionUtils.isEmpty(updateUsers)) { userService.batchUpdateUser(updateUsers); } // 新增打卡记录 if (!CollectionUtils.isEmpty(addPunchInRecords)) { punchInRecordService.batchInsert(addPunchInRecords); } if (!CollectionUtils.isEmpty(updatePunchInRecords)) { punchInRecordService.batchUpdate(updatePunchInRecords); } // 新增结算信息 if (!CollectionUtils.isEmpty(addPunchInSettlements)) { punchInSettlementService.batchInsert(addPunchInSettlements); } // 新增关联信息 if (!CollectionUtils.isEmpty(addRelas)) { punchInRecordSettlementRelaService.batchInsert(addRelas); } // 构造并新增结算任务信息 settlementTask.setProcessedSettleNum(addPunchInSettlements.size()); settlementTask.setProcessedUnsettleNum(settlementTask.getProcessedNum() - settlementTask.getProcessedSettleNum()); settlementTask.setEndTime(new Timestamp(System.currentTimeMillis())); settlementTaskService.update(settlementTask); log.info("结算任务结束"); } /** * 结算 * @param settleInfo 结算基本信息 * @param user 待结算的用户 * @param punchIns 打卡任务 * @param punchInRecordMap 打卡任务-打卡记录 map * @param weeklyPunchInRecordMap 打卡任务-一周打卡记录 map * @return */ private SettleResultDto settle(SettleInfoDto settleInfo, User user, List punchIns, Map punchInRecordMap, Map> weeklyPunchInRecordMap) { // 结算奖励数 int settleRewardNum = 0; // 新增的打卡结算关联关系 List addRelas = new ArrayList<>(); // 待更新的打卡记录 List updatePunchInRecords = new ArrayList<>(); // 待新增的打卡记录 List addPunchInRecords = new ArrayList<>(); // 结算 for (PunchIn punchIn : punchIns) { // 获取打卡记录 PunchInRecord punchInRecord = punchInRecordMap.get(punchIn.getId()); // 判断是否满足打卡规则 PunchInStatusEnum punchInStatus = judgePunchInStatus(punchIn, punchInRecord); //不满足则跳过无需接续,如果是补卡则直接完成继续计算 if (!PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType()) && PunchInStatusEnum.UN_FINISH.equals(punchInStatus)) { punchInRecord.setPunchInStatus(punchInStatus); updatePunchInRecords.add(punchInRecord); continue; } // 补打卡,完全没有打卡记录则需要补充打卡记录 if (PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType()) && Objects.isNull(punchInRecord)) { punchInRecord = new PunchInRecord(); punchInRecord.setPunchInId(punchIn.getId()); punchInRecord.setPunchInDate(settleInfo.getSettleDateStr()); addPunchInRecords.add(punchInRecord); } // 补打卡,已有打卡记录但是不满足打卡规则,则需要对打卡记录做准备 if (PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType()) && PunchInStatusEnum.UN_FINISH.equals(punchInStatus)) { punchInRecord.setPunchInStatus(PunchInStatusEnum.REMAKE_FINISH); fillTrack(punchIn, punchInRecord); } // 周末双倍奖励,否则计算普通奖励 settleRewardNum += settleInfo.getWeekendFlag() ? punchIn.getRewardNum() * 2 : punchIn.getRewardNum(); // 计算全勤双倍奖励 if (judgeFullAttendance(settleInfo, punchIn, weeklyPunchInRecordMap)) { settleRewardNum += punchIn.getRewardNum() * 2; } // 构建结算任务与记录关联信息 PunchInRecordSettlementRela rela = new PunchInRecordSettlementRela(); rela.setRecordId(punchInRecord.getId()); rela.setRewardNum(punchIn.getRewardNum()); rela.setCategory(punchIn.getCategory()); rela.setRule(punchIn.getRule()); addRelas.add(rela); } // 计算结算前后,用户奖励数的变化 int beforeSettleRewardNum = user.getUnclaimedRewardNum(); int afterSettleRewardNum = beforeSettleRewardNum + settleRewardNum; int totalRewardNum = user.getTotalRewardNum() + settleRewardNum; // 构造结算信息 PunchInSettlement addPunchInSettlement = new PunchInSettlement(); addPunchInSettlement.setUserId(user.getId()); addPunchInSettlement.setSettleRewardNum(settleRewardNum); addPunchInSettlement.setSettlementTime(new Timestamp(System.currentTimeMillis())); addPunchInSettlement.setBeforeSettleRewardNum(beforeSettleRewardNum); addPunchInSettlement.setAfterSettleRewardNum(afterSettleRewardNum); // 构造用户奖励信息 User updateUser = new User(); updateUser.setId(user.getId()); updateUser.setTotalRewardNum(totalRewardNum); updateUser.setUnclaimedRewardNum(afterSettleRewardNum); SettleResultDto settleResultDto = new SettleResultDto(); settleResultDto.setAddRelas(addRelas); settleResultDto.setUpdateUser(updateUser); settleResultDto.setAddPunchInRecords(addPunchInRecords); settleResultDto.setUpdatePunchInRecords(updatePunchInRecords); settleResultDto.setAddPunchInSettlements(addPunchInSettlement); return settleResultDto; } /** * 判断是否满足打卡规则完成打卡 * @param punchIn * @param punchInRecord * @return PunchInStatusEnum */ private PunchInStatusEnum judgePunchInStatus(PunchIn punchIn, PunchInRecord punchInRecord) { // 没有打卡记录,直接没完成,包含单次打卡的情况无需额外判断 if (Objects.isNull(punchInRecord)) { return PunchInStatusEnum.UN_FINISH; } // 计数打卡 if (PunchInCategoryEnum.COUNT.equals(punchIn.getCategory())) { if (PunchInRuleEnum.GREATER.equals(punchIn.getRule()) && punchInRecord.getCountTrack().compareTo(punchIn.getCountTrack()) < 1) { return PunchInStatusEnum.UN_FINISH; } if (PunchInRuleEnum.GREATER_OR_EQUAL.equals(punchIn.getRule()) && punchInRecord.getCountTrack().compareTo(punchIn.getCountTrack()) == -1) { return PunchInStatusEnum.UN_FINISH; } if (PunchInRuleEnum.LESS.equals(punchIn.getRule()) && punchInRecord.getCountTrack().compareTo(punchIn.getCountTrack()) > -1) { return PunchInStatusEnum.UN_FINISH; } if (PunchInRuleEnum.LESS_OR_EQUAL.equals(punchIn.getRule()) && punchInRecord.getCountTrack().compareTo(punchIn.getCountTrack()) == 1) { return PunchInStatusEnum.UN_FINISH; } return PunchInStatusEnum.FINISH; } // 计时打卡 if (PunchInCategoryEnum.TIME.equals(punchIn.getCategory())) { if (PunchInRuleEnum.GREATER.equals(punchIn.getRule()) && punchInRecord.getTimeTrack().compareTo(punchIn.getTimeTrack()) < 1) { return PunchInStatusEnum.UN_FINISH; } if (PunchInRuleEnum.GREATER_OR_EQUAL.equals(punchIn.getRule()) && punchInRecord.getTimeTrack().compareTo(punchIn.getTimeTrack()) == -1) { return PunchInStatusEnum.UN_FINISH; } if (PunchInRuleEnum.LESS.equals(punchIn.getRule()) && punchInRecord.getTimeTrack().compareTo(punchIn.getTimeTrack()) > -1) { return PunchInStatusEnum.UN_FINISH; } if (PunchInRuleEnum.LESS_OR_EQUAL.equals(punchIn.getRule()) && punchInRecord.getTimeTrack().compareTo(punchIn.getTimeTrack()) == 1) { return PunchInStatusEnum.UN_FINISH; } return PunchInStatusEnum.FINISH; } return PunchInStatusEnum.UN_FINISH; } /** * 填充打卡记录的记录信息(不用考虑单次打卡的情况) * @param punchIn * @param punchInRecord */ private void fillTrack(PunchIn punchIn, PunchInRecord punchInRecord) { // 计数打卡 if (PunchInCategoryEnum.COUNT.equals(punchIn.getCategory())) { if (PunchInRuleEnum.GREATER.equals(punchIn.getRule())) { punchInRecord.setCountTrack(punchIn.getCountTrack() + 1); } if (PunchInRuleEnum.LESS.equals(punchIn.getRule())){ punchInRecord.setCountTrack(punchIn.getCountTrack() - 1); } } // 计时打卡 if (PunchInCategoryEnum.TIME.equals(punchIn.getCategory())) { if (PunchInRuleEnum.GREATER.equals(punchIn.getRule())) { punchInRecord.setTimeTrack(punchIn.getTimeTrack().plusSeconds(1)); } if (PunchInRuleEnum.LESS.equals(punchIn.getRule())){ punchInRecord.setTimeTrack(punchIn.getTimeTrack().minusSeconds(1)); } } } /** * 判断是否进行全勤结算 * @param settleInfo 结算信息 * @param punchIn 打卡任务 * @param weeklyPunchInRecordMap 一周打卡记录 * @return true- */ private boolean judgeFullAttendance(SettleInfoDto settleInfo, PunchIn punchIn, Map> weeklyPunchInRecordMap) { // 没有启用全勤奖励则跳过 if (!punchIn.getFullAttendanceFlag()) { return false; } // 不是周日结算或者补打卡则跳过, if (!settleInfo.getSundayFlag() && !PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType())) { return false; } // 获取一周的完成打卡的打卡记录,并且要排除结算日这天的记录 List weeklyFinishRecord = Optional.ofNullable(weeklyPunchInRecordMap.get(punchIn.getId())).orElse(new ArrayList<>()) .stream().filter(record -> !settleInfo.getSettleDateStr().equals(record.getPunchInDate()) && (PunchInStatusEnum.FINISH.equals(record.getPunchInStatus()) || PunchInStatusEnum.REMAKE_FINISH.equals(record.getPunchInStatus()))) .collect(Collectors.toList()); // 1个是容错允许不打卡或未完成打卡,1个是当天的结算打卡来到这段逻辑就认为已经完成打卡,因此只要有5个完成打卡,则认为全勤 return weeklyFinishRecord.size() >= 5; } @Override public void manualSettle(SettleRequest settleRequest) { Assert.isNullInBusiness(settleRequest, "结算请求不能为空"); SpringUtils.getBean(ISettleManager.class).settleHandler(settleRequest.getSettleType(), LocalDate.parse(settleRequest.getSettleDate()), settleRequest.getUserIds(), settleRequest.getPunchInIds()); } @Override public List querySettle(SettleQuery query) { if (Objects.isNull(query) || !StringUtils.hasText(query.getStartDate()) || !StringUtils.hasText(query.getEndDate())) { BusinessException.throwFail("请选择待查询的结算记录时间范围"); } query.setStartDate(String.format("%s 00:00:00.000", query.getStartDate())); query.setEndDate(String.format("%s 23:59:59.999", query.getEndDate())); List punchInSettlements = punchInSettlementService.listByCondition(query); return punchInSettlements.stream().map(settlement -> { SettleDto dto = new SettleDto(); BeanUtils.copyProperties(settlement, dto); return dto; }).toList(); } }