SettleManagerImpl.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. package com.punchsettle.server.service.manager.impl;
  2. import java.sql.Timestamp;
  3. import java.time.DayOfWeek;
  4. import java.time.LocalDate;
  5. import java.util.ArrayList;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.Objects;
  9. import java.util.Optional;
  10. import java.util.function.Function;
  11. import java.util.stream.Collectors;
  12. import com.punchsettle.server.common.exception.BusinessException;
  13. import com.punchsettle.server.common.utils.Assert;
  14. import com.punchsettle.server.constant.PunchInCategoryEnum;
  15. import com.punchsettle.server.constant.PunchInRuleEnum;
  16. import com.punchsettle.server.constant.PunchInSettleTypeEnum;
  17. import com.punchsettle.server.constant.PunchInStatusEnum;
  18. import com.punchsettle.server.dto.settle.SettleDto;
  19. import com.punchsettle.server.dto.settle.SettleInfoDto;
  20. import com.punchsettle.server.dto.settle.SettleQuery;
  21. import com.punchsettle.server.dto.settle.SettleRequest;
  22. import com.punchsettle.server.dto.settle.SettleResultDto;
  23. import com.punchsettle.server.utiis.SpringUtils;
  24. import org.springframework.beans.BeanUtils;
  25. import org.springframework.beans.factory.annotation.Autowired;
  26. import org.springframework.stereotype.Service;
  27. import org.springframework.transaction.annotation.Transactional;
  28. import org.springframework.util.CollectionUtils;
  29. import com.punchsettle.server.atomic.entity.PunchIn;
  30. import com.punchsettle.server.atomic.entity.PunchInRecord;
  31. import com.punchsettle.server.atomic.entity.PunchInRecordSettlementRela;
  32. import com.punchsettle.server.atomic.entity.PunchInSettlement;
  33. import com.punchsettle.server.atomic.entity.SettlementTask;
  34. import com.punchsettle.server.atomic.entity.User;
  35. import com.punchsettle.server.atomic.service.IPunchInRecordService;
  36. import com.punchsettle.server.atomic.service.IPunchInRecordSettlementRelaService;
  37. import com.punchsettle.server.atomic.service.IPunchInService;
  38. import com.punchsettle.server.atomic.service.IPunchInSettlementService;
  39. import com.punchsettle.server.atomic.service.ISettlementTaskService;
  40. import com.punchsettle.server.atomic.service.IUserService;
  41. import com.punchsettle.server.dto.punchin.PunchInQuery;
  42. import com.punchsettle.server.dto.punchin.PunchInRecordQuery;
  43. import com.punchsettle.server.dto.task.SettleRewardTaskDto;
  44. import com.punchsettle.server.service.manager.ISettleManager;
  45. import com.punchsettle.server.utiis.DateUtils;
  46. import lombok.extern.slf4j.Slf4j;
  47. import org.springframework.util.StringUtils;
  48. /**
  49. * @author tyuio
  50. * @version 1.0.0
  51. * @description 结算服务类
  52. * @date 2024/12/12 22:29
  53. */
  54. @Slf4j
  55. @Service
  56. public class SettleManagerImpl implements ISettleManager {
  57. @Autowired
  58. private IPunchInService punchInService;
  59. @Autowired
  60. private IPunchInRecordService punchInRecordService;
  61. @Autowired
  62. private IUserService userService;
  63. @Autowired
  64. private IPunchInSettlementService punchInSettlementService;
  65. @Autowired
  66. private ISettlementTaskService settlementTaskService;
  67. @Autowired
  68. private IPunchInRecordSettlementRelaService punchInRecordSettlementRelaService;
  69. @Override
  70. @Transactional(rollbackFor = Exception.class)
  71. public void settleHandler(PunchInSettleTypeEnum settleType, LocalDate settleDate, List<Long> userIds, List<Long> punchInIds) {
  72. if (Objects.isNull(settleType)) {
  73. log.info("结算任务异常,原因:没有指定结算类型");
  74. return;
  75. }
  76. if (Objects.isNull(settleDate)) {
  77. log.info("结算任务异常,原因:没有指定结算日期");
  78. return;
  79. }
  80. if (settleDate.isAfter(LocalDate.now())) {
  81. BusinessException.throwFail(String.format("结算任务异常,原因:结算日期{} 不能大于等于今天", settleDate));
  82. }
  83. log.info("结算任务开始,结算类型:{}, 结算日期:{}", settleType.getName(), settleDate);
  84. Timestamp settleStartTime = new Timestamp(System.currentTimeMillis());
  85. SettleInfoDto settleInfo = new SettleInfoDto(settleType, settleDate);
  86. // 读取用户数据,如果userIds为空则结算所有的用户
  87. userIds = Optional.ofNullable(userIds).orElseGet(() -> {
  88. PunchInRecordQuery recordQuery = new PunchInRecordQuery();
  89. recordQuery.setStartDate(settleInfo.getSettleDateStr());
  90. recordQuery.setEndDate(settleInfo.getSettleDateStr());
  91. List<PunchInRecord> punchInRecords = punchInRecordService.listByCondition(recordQuery);
  92. return punchInRecords.stream().map(PunchInRecord::getCreatedBy).collect(Collectors.toList());
  93. });
  94. List<User> users = userService.listByIds(userIds);
  95. if (CollectionUtils.isEmpty(users)) {
  96. log.info("结算任务结束,原因:没有找到待结算的用户信息");
  97. return;
  98. }
  99. // 读取用户的打卡任务,如果punchIds为空则结算用户所有的打卡任务
  100. PunchInQuery punchInQuery = new PunchInQuery();
  101. punchInQuery.setUserIds(userIds);
  102. punchInQuery.setPunchInIds(punchInIds);
  103. List<PunchIn> punchIns = punchInService.listByCondition(punchInQuery);
  104. if (CollectionUtils.isEmpty(punchIns)) {
  105. log.info("结算任务结束,原因:没有找到打卡任务");
  106. return;
  107. }
  108. // 获取最新的打卡任务ID
  109. punchInIds = punchIns.stream().map(PunchIn::getId).collect(Collectors.toList());
  110. // 获取结算日的打卡记录
  111. PunchInRecordQuery recordQuery = new PunchInRecordQuery();
  112. recordQuery.setStartDate(settleInfo.getSettleDateStr());
  113. recordQuery.setEndDate(settleInfo.getSettleDateStr());
  114. recordQuery.setPunchInIds(punchInIds);
  115. List<PunchInRecord> punchInRecords = punchInRecordService.listByCondition(recordQuery);
  116. if (CollectionUtils.isEmpty(punchInRecords)) {
  117. log.info("结算任务结束,原因:没有打卡记录");
  118. return;
  119. }
  120. // 获取一周的打卡记录
  121. Map<Long, List<PunchInRecord>> weeklyPunchInRecords = Map.of();
  122. if (settleInfo.getSundayFlag() || PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType())) {
  123. PunchInRecordQuery weeklyRecordQuery = new PunchInRecordQuery();
  124. weeklyRecordQuery.setPunchInIds(punchInIds);
  125. weeklyRecordQuery.setStartDate(DateUtils.getLastWeekMonday(settleInfo.getSettleDate()).toString());
  126. weeklyRecordQuery.setEndDate(DateUtils.getLastWeekSunday(settleInfo.getSettleDate()).toString());
  127. List<PunchInRecord> punchInRecordForWeeks = punchInRecordService.listByCondition(weeklyRecordQuery);
  128. if (!CollectionUtils.isEmpty(punchInRecordForWeeks)) {
  129. weeklyPunchInRecords = punchInRecordForWeeks.stream().collect(Collectors.groupingBy(PunchInRecord::getPunchInId));
  130. }
  131. }
  132. // 用户-打卡任务分组
  133. Map<Long, List<PunchIn>> userPunchInMap = punchIns.stream().collect(Collectors.groupingBy(PunchIn::getCreatedBy));
  134. // 打卡任务-打卡记录 map
  135. Map<Long, PunchInRecord> punchInRecordMap = punchInRecords.stream().collect(Collectors.toMap(PunchInRecord::getPunchInId, Function.identity(), (key1, key2) -> key1));
  136. // 待更新的用户
  137. List<User> updateUsers = new ArrayList<>();
  138. // 新增的打卡结算关联关系
  139. List<PunchInRecordSettlementRela> addRelas = new ArrayList<>();
  140. // 待更新的打卡记录
  141. List<PunchInRecord> updatePunchInRecords = new ArrayList<>();
  142. // 待新增的打卡记录
  143. List<PunchInRecord> addPunchInRecords = new ArrayList<>();
  144. // 待新增的打卡结算信息
  145. List<PunchInSettlement> addPunchInSettlements = new ArrayList<>();
  146. // 先创建结算任务执行记录
  147. SettlementTask settlementTask = new SettlementTask();
  148. settlementTask.setSettleDate(settleDate.toString());
  149. settlementTask.setStartTime(settleStartTime);
  150. settlementTask.setProcessedNum(users.size());
  151. settlementTaskService.insert(settlementTask);
  152. // 结算
  153. for (User user : users) {
  154. SettleResultDto settleResult = settle(settleInfo, user, userPunchInMap.get(user.getId()), punchInRecordMap, weeklyPunchInRecords);
  155. updateUsers.add(settleResult.getUpdateUser());
  156. addRelas.addAll(settleResult.getAddRelas());
  157. addPunchInRecords.addAll(settleResult.getAddPunchInRecords());
  158. updatePunchInRecords.addAll(settleResult.getUpdatePunchInRecords());
  159. addPunchInSettlements.add(settleResult.getAddPunchInSettlements());
  160. }
  161. // 更新用户奖励信息
  162. if (!CollectionUtils.isEmpty(updateUsers)) {
  163. userService.batchUpdateUser(updateUsers);
  164. }
  165. // 新增打卡记录
  166. if (!CollectionUtils.isEmpty(addPunchInRecords)) {
  167. punchInRecordService.batchInsert(addPunchInRecords);
  168. }
  169. if (!CollectionUtils.isEmpty(updatePunchInRecords)) {
  170. punchInRecordService.batchUpdate(updatePunchInRecords);
  171. }
  172. // 新增结算信息
  173. if (!CollectionUtils.isEmpty(addPunchInSettlements)) {
  174. punchInSettlementService.batchInsert(addPunchInSettlements);
  175. }
  176. // 新增关联信息
  177. if (!CollectionUtils.isEmpty(addRelas)) {
  178. punchInRecordSettlementRelaService.batchInsert(addRelas);
  179. }
  180. // 构造并新增结算任务信息
  181. settlementTask.setProcessedSettleNum(addPunchInSettlements.size());
  182. settlementTask.setProcessedUnsettleNum(settlementTask.getProcessedNum() - settlementTask.getProcessedSettleNum());
  183. settlementTask.setEndTime(new Timestamp(System.currentTimeMillis()));
  184. settlementTaskService.update(settlementTask);
  185. log.info("结算任务结束");
  186. }
  187. /**
  188. * 结算
  189. * @param settleInfo 结算基本信息
  190. * @param user 待结算的用户
  191. * @param punchIns 打卡任务
  192. * @param punchInRecordMap 打卡任务-打卡记录 map
  193. * @param weeklyPunchInRecordMap 打卡任务-一周打卡记录 map
  194. * @return
  195. */
  196. private SettleResultDto settle(SettleInfoDto settleInfo, User user, List<PunchIn> punchIns, Map<Long, PunchInRecord> punchInRecordMap, Map<Long, List<PunchInRecord>> weeklyPunchInRecordMap) {
  197. // 结算奖励数
  198. int settleRewardNum = 0;
  199. // 新增的打卡结算关联关系
  200. List<PunchInRecordSettlementRela> addRelas = new ArrayList<>();
  201. // 待更新的打卡记录
  202. List<PunchInRecord> updatePunchInRecords = new ArrayList<>();
  203. // 待新增的打卡记录
  204. List<PunchInRecord> addPunchInRecords = new ArrayList<>();
  205. // 结算
  206. for (PunchIn punchIn : punchIns) {
  207. // 获取打卡记录
  208. PunchInRecord punchInRecord = punchInRecordMap.get(punchIn.getId());
  209. // 判断是否满足打卡规则
  210. PunchInStatusEnum punchInStatus = judgePunchInStatus(punchIn, punchInRecord);
  211. //不满足则跳过无需接续,如果是补卡则直接完成继续计算
  212. if (!PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType()) && PunchInStatusEnum.UN_FINISH.equals(punchInStatus)) {
  213. punchInRecord.setPunchInStatus(punchInStatus);
  214. updatePunchInRecords.add(punchInRecord);
  215. continue;
  216. }
  217. // 补打卡,完全没有打卡记录则需要补充打卡记录
  218. if (PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType()) && Objects.isNull(punchInRecord)) {
  219. punchInRecord = new PunchInRecord();
  220. punchInRecord.setPunchInId(punchIn.getId());
  221. punchInRecord.setPunchInDate(settleInfo.getSettleDateStr());
  222. addPunchInRecords.add(punchInRecord);
  223. }
  224. // 补打卡,已有打卡记录但是不满足打卡规则,则需要对打卡记录做准备
  225. if (PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType()) && PunchInStatusEnum.UN_FINISH.equals(punchInStatus)) {
  226. punchInRecord.setPunchInStatus(PunchInStatusEnum.REMAKE_FINISH);
  227. fillTrack(punchIn, punchInRecord);
  228. }
  229. // 周末双倍奖励,否则计算普通奖励
  230. settleRewardNum += settleInfo.getWeekendFlag() ? punchIn.getRewardNum() * 2 : punchIn.getRewardNum();
  231. // 计算全勤双倍奖励
  232. if (judgeFullAttendance(settleInfo, punchIn, weeklyPunchInRecordMap)) {
  233. settleRewardNum += punchIn.getRewardNum() * 2;
  234. }
  235. // 构建结算任务与记录关联信息
  236. PunchInRecordSettlementRela rela = new PunchInRecordSettlementRela();
  237. rela.setRecordId(punchInRecord.getId());
  238. rela.setRewardNum(punchIn.getRewardNum());
  239. rela.setCategory(punchIn.getCategory());
  240. rela.setRule(punchIn.getRule());
  241. addRelas.add(rela);
  242. }
  243. // 计算结算前后,用户奖励数的变化
  244. int beforeSettleRewardNum = user.getUnclaimedRewardNum();
  245. int afterSettleRewardNum = beforeSettleRewardNum + settleRewardNum;
  246. int totalRewardNum = user.getTotalRewardNum() + settleRewardNum;
  247. // 构造结算信息
  248. PunchInSettlement addPunchInSettlement = new PunchInSettlement();
  249. addPunchInSettlement.setUserId(user.getId());
  250. addPunchInSettlement.setSettleRewardNum(settleRewardNum);
  251. addPunchInSettlement.setSettlementTime(new Timestamp(System.currentTimeMillis()));
  252. addPunchInSettlement.setBeforeSettleRewardNum(beforeSettleRewardNum);
  253. addPunchInSettlement.setAfterSettleRewardNum(afterSettleRewardNum);
  254. // 构造用户奖励信息
  255. User updateUser = new User();
  256. updateUser.setId(user.getId());
  257. updateUser.setTotalRewardNum(totalRewardNum);
  258. updateUser.setUnclaimedRewardNum(afterSettleRewardNum);
  259. SettleResultDto settleResultDto = new SettleResultDto();
  260. settleResultDto.setAddRelas(addRelas);
  261. settleResultDto.setUpdateUser(updateUser);
  262. settleResultDto.setAddPunchInRecords(addPunchInRecords);
  263. settleResultDto.setUpdatePunchInRecords(updatePunchInRecords);
  264. settleResultDto.setAddPunchInSettlements(addPunchInSettlement);
  265. return settleResultDto;
  266. }
  267. /**
  268. * 判断是否满足打卡规则完成打卡
  269. * @param punchIn
  270. * @param punchInRecord
  271. * @return PunchInStatusEnum
  272. */
  273. private PunchInStatusEnum judgePunchInStatus(PunchIn punchIn, PunchInRecord punchInRecord) {
  274. // 没有打卡记录,直接没完成,包含单次打卡的情况无需额外判断
  275. if (Objects.isNull(punchInRecord)) {
  276. return PunchInStatusEnum.UN_FINISH;
  277. }
  278. // 计数打卡
  279. if (PunchInCategoryEnum.COUNT.equals(punchIn.getCategory())) {
  280. if (PunchInRuleEnum.GREATER.equals(punchIn.getRule()) && punchInRecord.getCountTrack().compareTo(punchIn.getCountTrack()) < 1) {
  281. return PunchInStatusEnum.UN_FINISH;
  282. }
  283. if (PunchInRuleEnum.GREATER_OR_EQUAL.equals(punchIn.getRule()) && punchInRecord.getCountTrack().compareTo(punchIn.getCountTrack()) == -1) {
  284. return PunchInStatusEnum.UN_FINISH;
  285. }
  286. if (PunchInRuleEnum.LESS.equals(punchIn.getRule()) && punchInRecord.getCountTrack().compareTo(punchIn.getCountTrack()) > -1) {
  287. return PunchInStatusEnum.UN_FINISH;
  288. }
  289. if (PunchInRuleEnum.LESS_OR_EQUAL.equals(punchIn.getRule()) && punchInRecord.getCountTrack().compareTo(punchIn.getCountTrack()) == 1) {
  290. return PunchInStatusEnum.UN_FINISH;
  291. }
  292. return PunchInStatusEnum.FINISH;
  293. }
  294. // 计时打卡
  295. if (PunchInCategoryEnum.TIME.equals(punchIn.getCategory())) {
  296. if (PunchInRuleEnum.GREATER.equals(punchIn.getRule()) && punchInRecord.getTimeTrack().compareTo(punchIn.getTimeTrack()) < 1) {
  297. return PunchInStatusEnum.UN_FINISH;
  298. }
  299. if (PunchInRuleEnum.GREATER_OR_EQUAL.equals(punchIn.getRule()) && punchInRecord.getTimeTrack().compareTo(punchIn.getTimeTrack()) == -1) {
  300. return PunchInStatusEnum.UN_FINISH;
  301. }
  302. if (PunchInRuleEnum.LESS.equals(punchIn.getRule()) && punchInRecord.getTimeTrack().compareTo(punchIn.getTimeTrack()) > -1) {
  303. return PunchInStatusEnum.UN_FINISH;
  304. }
  305. if (PunchInRuleEnum.LESS_OR_EQUAL.equals(punchIn.getRule()) && punchInRecord.getTimeTrack().compareTo(punchIn.getTimeTrack()) == 1) {
  306. return PunchInStatusEnum.UN_FINISH;
  307. }
  308. return PunchInStatusEnum.FINISH;
  309. }
  310. return PunchInStatusEnum.UN_FINISH;
  311. }
  312. /**
  313. * 填充打卡记录的记录信息(不用考虑单次打卡的情况)
  314. * @param punchIn
  315. * @param punchInRecord
  316. */
  317. private void fillTrack(PunchIn punchIn, PunchInRecord punchInRecord) {
  318. // 计数打卡
  319. if (PunchInCategoryEnum.COUNT.equals(punchIn.getCategory())) {
  320. if (PunchInRuleEnum.GREATER.equals(punchIn.getRule())) {
  321. punchInRecord.setCountTrack(punchIn.getCountTrack() + 1);
  322. }
  323. if (PunchInRuleEnum.LESS.equals(punchIn.getRule())){
  324. punchInRecord.setCountTrack(punchIn.getCountTrack() - 1);
  325. }
  326. }
  327. // 计时打卡
  328. if (PunchInCategoryEnum.TIME.equals(punchIn.getCategory())) {
  329. if (PunchInRuleEnum.GREATER.equals(punchIn.getRule())) {
  330. punchInRecord.setTimeTrack(punchIn.getTimeTrack().plusSeconds(1));
  331. }
  332. if (PunchInRuleEnum.LESS.equals(punchIn.getRule())){
  333. punchInRecord.setTimeTrack(punchIn.getTimeTrack().minusSeconds(1));
  334. }
  335. }
  336. }
  337. /**
  338. * 判断是否进行全勤结算
  339. * @param settleInfo 结算信息
  340. * @param punchIn 打卡任务
  341. * @param weeklyPunchInRecordMap 一周打卡记录
  342. * @return true-
  343. */
  344. private boolean judgeFullAttendance(SettleInfoDto settleInfo, PunchIn punchIn, Map<Long, List<PunchInRecord>> weeklyPunchInRecordMap) {
  345. // 没有启用全勤奖励则跳过
  346. if (!punchIn.getFullAttendanceFlag()) {
  347. return false;
  348. }
  349. // 不是周日结算或者补打卡则跳过,
  350. if (!settleInfo.getSundayFlag() && !PunchInSettleTypeEnum.REMAKE.equals(settleInfo.getSettleType())) {
  351. return false;
  352. }
  353. // 获取一周的完成打卡的打卡记录,并且要排除结算日这天的记录
  354. List<PunchInRecord> weeklyFinishRecord = Optional.ofNullable(weeklyPunchInRecordMap.get(punchIn.getId())).orElse(new ArrayList<>())
  355. .stream().filter(record -> !settleInfo.getSettleDateStr().equals(record.getPunchInDate()) && (PunchInStatusEnum.FINISH.equals(record.getPunchInStatus()) || PunchInStatusEnum.REMAKE_FINISH.equals(record.getPunchInStatus())))
  356. .collect(Collectors.toList());
  357. // 1个是容错允许不打卡或未完成打卡,1个是当天的结算打卡来到这段逻辑就认为已经完成打卡,因此只要有5个完成打卡,则认为全勤
  358. return weeklyFinishRecord.size() >= 5;
  359. }
  360. @Override
  361. public void manualSettle(SettleRequest settleRequest) {
  362. Assert.isNullInBusiness(settleRequest, "结算请求不能为空");
  363. SpringUtils.getBean(ISettleManager.class).settleHandler(settleRequest.getSettleType(), LocalDate.parse(settleRequest.getSettleDate()), settleRequest.getUserIds(), settleRequest.getPunchInIds());
  364. }
  365. @Override
  366. public List<SettleDto> querySettle(SettleQuery query) {
  367. if (Objects.isNull(query) || !StringUtils.hasText(query.getStartDate()) || !StringUtils.hasText(query.getEndDate())) {
  368. BusinessException.throwFail("请选择待查询的结算记录时间范围");
  369. }
  370. query.setStartDate(String.format("%s 00:00:00.000", query.getStartDate()));
  371. query.setEndDate(String.format("%s 23:59:59.999", query.getEndDate()));
  372. List<PunchInSettlement> punchInSettlements = punchInSettlementService.listByCondition(query);
  373. return punchInSettlements.stream().map(settlement -> {
  374. SettleDto dto = new SettleDto();
  375. BeanUtils.copyProperties(settlement, dto);
  376. return dto;
  377. }).toList();
  378. }
  379. }