FKLImplService.java 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. package com.malk.fengkaili.service.impl;
  2. import cn.hutool.core.util.ObjectUtil;
  3. import com.malk.fengkaili.repository.dao.FKLDdContactDao;
  4. import com.malk.fengkaili.repository.entity.FKLDdContactPo;
  5. import com.malk.fengkaili.service.FKLService;
  6. import com.malk.service.dingtalk.DDClient;
  7. import com.malk.service.dingtalk.DDClient_Attendance;
  8. import com.malk.service.dingtalk.DDClient_Contacts;
  9. import com.malk.service.dingtalk.DDService;
  10. import com.malk.utils.UtilDateTime;
  11. import com.malk.utils.UtilList;
  12. import com.malk.utils.UtilMap;
  13. import com.malk.utils.UtilNumber;
  14. import lombok.extern.slf4j.Slf4j;
  15. import org.apache.commons.lang3.ObjectUtils;
  16. import org.apache.commons.lang3.StringUtils;
  17. import org.springframework.beans.factory.annotation.Autowired;
  18. import org.springframework.data.domain.Page;
  19. import org.springframework.data.domain.PageRequest;
  20. import org.springframework.data.domain.Pageable;
  21. import org.springframework.data.domain.Sort;
  22. import org.springframework.data.jpa.domain.Specification;
  23. import org.springframework.stereotype.Service;
  24. import javax.persistence.criteria.Predicate;
  25. import java.time.Duration;
  26. import java.time.LocalDate;
  27. import java.time.LocalDateTime;
  28. import java.util.*;
  29. import java.util.concurrent.atomic.AtomicReference;
  30. import java.util.stream.Collectors;
  31. @Service
  32. @Slf4j
  33. public class FKLImplService implements FKLService {
  34. @Autowired
  35. private DDClient ddClient;
  36. @Autowired
  37. private DDClient_Contacts ddClient_contacts;
  38. @Autowired
  39. private FKLDdContactDao fklDdContactDao;
  40. @Autowired
  41. private DDClient_Attendance ddClient_attendance;
  42. @Autowired
  43. private DDService ddService;
  44. /**
  45. * 同步用户信息
  46. */
  47. @Override
  48. public void syncUserInfo() {
  49. // 匹配部门信息, 全量
  50. ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true).forEach(deptId -> {
  51. // String deptName = ddClient_contacts.getDepartmentInfo(ddClient.getAccessToken(), deptId).get("name").toString();
  52. List<String> userIds = ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), deptId);
  53. if (userIds.size() > 0) {
  54. // 获取部门层级拼接
  55. String deptName = ddService.getUserDepartmentHierarchyJoin(ddClient.getAccessToken(), userIds.get(0), "-");
  56. for (String userId : userIds) {
  57. // 牧语
  58. if ("0953580166811961653".equals(userId) || fklDdContactDao.existsByUserId(userId)) {
  59. continue;
  60. }
  61. Map userinfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), userId);
  62. // 员工信息表, 落库
  63. fklDdContactDao.save(FKLDdContactPo.builder()
  64. .userId(userId)
  65. .name(UtilMap.getString(userinfo, "name"))
  66. .jobNumber(UtilMap.getString(userinfo, "job_number"))
  67. .deptId(deptId)
  68. .deptName(deptName)
  69. .mobile(UtilMap.getString(userinfo, "mobile"))
  70. .hiredDate(userinfo.containsKey("hired_date") ? new Date(UtilMap.getLong(userinfo, "hired_date")) : null)
  71. .remark(UtilMap.getString(userinfo, "remark")) // 无需打卡 标记
  72. .build());
  73. log.info("同步#入职人员, {}", userinfo);
  74. }
  75. }
  76. });
  77. // 同步离职人员, 标记离职日期
  78. Date start = UtilDateTime.convertToDateFromLocalDateTime(UtilDateTime.firstDayOfLastMonth(LocalDateTime.now()));
  79. ddClient_contacts.getLeaveEmployeeRecords(ddClient.getAccessToken(), start, null).forEach(item -> {
  80. log.info("同步#离职人员, {}", item);
  81. fklDdContactDao.updateLeaveDate(item.get("userId"), UtilDateTime.parse(item.get("leaveTime"), "yyyy-MM-dd'T'HH:mm:ss"));
  82. });
  83. }
  84. /**
  85. * 查询用户列表
  86. */
  87. @Override
  88. public Page<FKLDdContactPo> queryUserInfos(int page, int size, String name, List<Long> deptIds) {
  89. // 分页 & 排序
  90. Sort sort = Sort.by(Sort.Direction.ASC, "deptName");
  91. Pageable pageable = PageRequest.of(page - 1, size, sort);
  92. // 查询条件: 姓名, 所属部门
  93. Specification<FKLDdContactPo> specification = (root, criteriaQuery, criteriaBuilder) -> {
  94. List<Predicate> predicateList = new ArrayList<>();
  95. if (StringUtils.isNotBlank(name)) {
  96. predicateList.add(criteriaBuilder.equal(root.get("name"), name));
  97. }
  98. if (UtilList.isNotEmpty(deptIds)) {
  99. predicateList.add(criteriaBuilder.in(root.get("deptId")).value(deptIds));
  100. }
  101. return criteriaBuilder.and(predicateList.toArray(new javax.persistence.criteria.Predicate[predicateList.size()]));
  102. };
  103. // 无数据时返回空列表
  104. return fklDdContactDao.findAll(specification, pageable);
  105. }
  106. /// 累计月度汇总数字
  107. private Object _reduceAttendance(Map column, String name, String keyList) {
  108. Object value;
  109. List<Map> vals = (List<Map>) column.get(keyList);
  110. // 异常信息, 保留备注
  111. if (name.equals("考勤结果")) {
  112. List<String> tmps = new ArrayList<>(); // 同行出差会重复, 考勤结果要过滤
  113. vals.stream().forEach(item -> {
  114. // prd 异常补录当前日期
  115. String content = UtilMap.getString(item, "value");
  116. String svalue = content;
  117. if (!content.contains("-")) {
  118. content += UtilMap.getString(item, "date").split(" ")[0];
  119. }
  120. content = content.replace("未打卡,", "").replace("正常,", "").replace("休息并打卡,", "").replace("休息,", "");
  121. // 休息有外出/出差 , 正常带其他状态情况 || 超过90未打卡静默用户 || 被添加为协同人后, 钉钉也会记录一条出差
  122. if (content.contains("出差")) {
  123. // 兼容出差中还有其他考勤结果, 以及还存在跨天的情况下
  124. List<String> arr = new ArrayList<>();
  125. for (String t : content.split(",")) {
  126. if (!arr.contains(t) && !tmps.stream().filter(s -> s.contains(t)).findAny().isPresent()) {
  127. arr.add(t);
  128. }
  129. }
  130. if (arr.size() == 0) {
  131. return;
  132. }
  133. content = String.join(",", arr);
  134. }
  135. boolean isFuture = UtilDateTime.parseLocalDateTime(UtilMap.getString(item, "date")).isAfter(LocalDateTime.now());
  136. if (!isFuture && StringUtils.isNotBlank(svalue) && !tmps.contains(content) && !content.contains("休息") && !svalue.equals("正常") && !svalue.equals("未打卡")) {
  137. tmps.add(content);
  138. }
  139. });
  140. value = String.join("; ", tmps);
  141. } else {
  142. value = vals.stream().map(item -> UtilMap.getFloat(item, "value")).reduce(0.f, (a, b) -> {
  143. // ddExt: 出差默认是可重复提交, 且若被添加为协同人, 也会多累计一天出差 [但工作时长是正常]. 可开启不允许重复提交, 同样的同行人会冲突
  144. if (name.equals("出差时长") && b > 1.0f) {
  145. b = 1.0f;
  146. }
  147. return a + b;
  148. });
  149. }
  150. return value;
  151. }
  152. /// 缓存考勤自定义列
  153. private List<Map> columns;
  154. List<Map> getColumns() {
  155. if (UtilList.isEmpty(columns)) {
  156. columns = ddClient_attendance.getAttColumns(ddClient.getAccessToken());
  157. }
  158. return columns;
  159. }
  160. /**
  161. * 考勤数据统计
  162. */
  163. @Override
  164. public List<Map> queryAttendanceList(String start, String end, List<FKLDdContactPo> userInfos, List<String> days) {
  165. // 考勤列, 假期信息定义
  166. List<String> columnNames = Arrays.asList("旷工天数", "出勤天数", "工作时长", "考勤结果", "出差时长", "迟到次数", "早退次数", "下班缺卡次数", "上班缺卡次数", "外出时长", "休息日加班", "工作日加班", "节假日加班", "严重迟到次数", "应出勤天数");
  167. AtomicReference<String> fileId_attendance_days = new AtomicReference<>(""); // 出勤天数字段id
  168. AtomicReference<String> fileId_attendance_result = new AtomicReference<>(""); // 考勤结果字段id
  169. List<Map> columns = getColumns();
  170. Map columnIds = new HashMap();
  171. // 假期单独返回, 钉钉产品规则
  172. List<String> leaveNames = columns.stream().filter(column -> {
  173. if ("出勤天数".equals(column.get("name"))) {
  174. fileId_attendance_days.set(String.valueOf(column.get("id")));
  175. }
  176. if ("考勤结果".equals(column.get("name"))) {
  177. fileId_attendance_result.set(String.valueOf(column.get("id")));
  178. }
  179. // 列类型储存id映射名称为map, 考勤数据返回仅保留列id
  180. if (columnNames.contains(column.get("name"))) {
  181. columnIds.put(column.get("id").toString(), column.get("name"));
  182. return false;
  183. }
  184. return column.get("alias").equals("leave_");
  185. }
  186. ).map(column -> String.valueOf(column.get("name"))).collect(Collectors.toList());
  187. // 考勤汇总数据
  188. List<Map> attendanceInfos = new ArrayList<>();
  189. List<String> queryIds = new ArrayList<>(columnIds.keySet()); // 考勤列定义
  190. userInfos.forEach(po -> {
  191. Map attendanceInfo = UtilMap.map("员工ID, 员工姓名, 员工工号, 所属部门, 考勤状态", po.getUserId(), po.getName(), po.getJobNumber(), po.getDeptName(), po.getRemark());
  192. // 累计月度汇总
  193. List<Map> attendanceList = ddClient_attendance.getAttColumnVal(ddClient.getAccessToken(), po.getUserId(), queryIds, start, end);
  194. attendanceList.forEach(column -> {
  195. String id = ((Map) column.get("column_vo")).get("id").toString();
  196. String name = String.valueOf(columnIds.get(id)); // 接口仅返回列id, 通过map映射
  197. attendanceInfo.put(name, _reduceAttendance(column, name, "column_vals"));
  198. // prd [sheet2]每天考勤结果统计
  199. if (!Objects.isNull(days) && name.equals("考勤结果")) {
  200. List<Map> vals = (List<Map>) column.get("column_vals");
  201. int index = 0;
  202. for (Map<String, String> val : vals) {
  203. index++;
  204. String date = val.get("date").replace(" 00:00:00", "").replace(LocalDate.now().getYear() + "-", "");
  205. String result = val.get("value").replace("休息并打卡,", "").replace("休息,", ""); // 休息有外出/出差;
  206. log.info("人员明细, {} - {}, {}", date, po.getName(), val.get("value"));
  207. String day_1 = "zc", day_2 = "zc", type = "zc"; // 异常类型
  208. if (result.contains("休息") || result.contains("加班") || (val.get("value").contains("休息,") && (!result.contains("出差") && !result.contains("婚假") && !result.contains("产假")))) {
  209. type = "公假"; // 包含休息, 休息加班打卡, 忽略跨休息日连续请假情况, prd 钉钉后台配置: 产假, 婚假按自然日
  210. day_1 = type;
  211. day_2 = type;
  212. } else if (StringUtils.isBlank(result)) {
  213. type = "/"; // 新入职
  214. day_1 = "/";
  215. day_2 = "/";
  216. } else if (result.equals("正常") || (result.split(",").length == 2 && result.contains("外勤") && result.contains("补卡")) || result.equals("下班外勤") || result.equals("上班外勤") || result.equals("上班外勤,下班外勤")) {
  217. // 包含补卡, 一次外勤补卡, 外勤考勤情况 [调休会被标识为考勤正常]
  218. type = "zc";
  219. day_1 = type;
  220. day_2 = type;
  221. } else if (result.contains("产假") || result.contains("陪产假") || result.contains("婚假") || result.contains("丧假")) {
  222. type = result.split("假")[0] + "假"; // 按天请假
  223. day_1 = type;
  224. day_2 = type;
  225. } else if (result.contains("旷工") || result.equals("未打卡")) {
  226. type = "旷工"; // 兼容异常情况
  227. day_1 = type;
  228. day_2 = type;
  229. } else if (result.contains("缺卡") && !result.contains("到")) {
  230. // prd 8点上班, 8点后请假或外出都是缺卡记录
  231. if (result.equals("上班缺卡")) {
  232. type = "缺卡";
  233. day_1 = type;
  234. }
  235. if (result.equals("下班缺卡")) {
  236. // prd 离职操作是直接删除, 会有一次打卡, 符合标记为zc
  237. if (ObjectUtil.isNotNull(po.getLeaveDate()) && date.equals(UtilDateTime.format(po.getLeaveDate(), "MM-dd"))) {
  238. type = type.length() > 0 ? type : "zc";
  239. day_2 = "zc";
  240. } else {
  241. type = "缺卡";
  242. day_2 = type;
  243. }
  244. }
  245. } else if (result.split(",").length <= 2 && (result.contains("迟到") || result.contains("早退"))) {
  246. // 兼容早退和迟到情况下, 还存在请假情况
  247. if (result.contains("迟到") && !result.contains("补卡申请")) {
  248. type = "迟到"; // 迟到状态标记
  249. float exception_duration = Float.valueOf((result.split(",")[0].split("分钟")[0].replace("上班迟到", "").replace("上班严重迟到", "")));
  250. if (exception_duration >= 180f) {
  251. // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
  252. day_1 = "迟到";
  253. }
  254. }
  255. if (result.contains("早退") && !result.contains("补卡申请")) {
  256. type += type.length() > 0 ? " 早退" : "早退"; // 早退状态标记
  257. float exception_duration = Float.valueOf((result.split(",")[result.split(",").length - 1].split("分钟")[0].replace("下班早退", "")));
  258. if (exception_duration >= 180f) {
  259. // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
  260. day_2 = "早退";
  261. }
  262. }
  263. } else {
  264. type = "";
  265. day_1 = "";
  266. day_2 = "";
  267. // 请假 & 出差
  268. for (String status : result.split(",")) {
  269. /// 过滤异常情况 & 未打卡判定为status, 非result & 外勤又外出情况
  270. if (status.contains("补卡申请") || status.contains("正常") || status.equals("未打卡") || status.contains("外勤")) {
  271. continue;
  272. }
  273. if (status.contains("缺卡") || status.equals("未打卡") || status.contains("迟到") || status.contains("早退")) {
  274. if (status.equals("上班缺卡")) {
  275. type = "缺卡";
  276. day_1 = "缺卡";
  277. }
  278. if (status.equals("下班缺卡")) {
  279. type += "缺卡";
  280. day_2 = "缺卡";
  281. }
  282. // 兼容早退和迟到情况下, 还存在请假情况
  283. if (status.contains("迟到") || status.contains("早退")) {
  284. if (status.contains("迟到")) {
  285. type = "迟到"; // 迟到状态标记
  286. float exception_duration = Float.valueOf((status.split(",")[0].split("分钟")[0].replace("上班迟到", "").replace("上班严重迟到", "")));
  287. if (exception_duration >= 180f) {
  288. // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
  289. day_1 = "迟到";
  290. }
  291. }
  292. if (status.contains("早退")) {
  293. type += type.length() > 0 ? " 早退" : "早退"; // 早退状态标记
  294. float exception_duration = Float.valueOf((status.split(",")[0].split("分钟")[0].replace("下班早退", "")));
  295. if (exception_duration >= 180f) {
  296. // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
  297. day_2 = "早提";
  298. }
  299. }
  300. }
  301. } else {
  302. /// 请假数据处理 [小时情况]
  303. String tmp = status.contains("调休") ? "调休" : status.split("假")[0] + "假"; // 异常类型
  304. if (status.contains("外出") || Arrays.asList("调休", "哺乳假", "事假").contains(tmp)) {
  305. if (result.contains("外出")) {
  306. tmp = "外出";
  307. }
  308. // 外出, 调休, 事假, 哺乳假: 兼容9点申请, 排班是8点情况, 不记录缺卡
  309. if (day_1.equals("缺卡") && result.contains("09:00")) {
  310. day_1 = "";
  311. }
  312. // prd 请假3小时以内标记为zc, 按照小时请假 [调休, 哺乳假, 事假];
  313. String[] arr = status.split(" ");
  314. float hour = Float.valueOf((arr[arr.length - 1].replace("小时", "")));
  315. if (hour < 3.0f && !tmp.equals("外出")) { // <3 同时9点申请标识zc, 避免不能统计外出情况
  316. continue;
  317. } else {
  318. // prd 请假3小时以内标记为zc, 区分上午与下午, 午休从12-13分割
  319. String sStart = status.split(" ")[1].split("到")[0].replace(":", "");
  320. type = type.length() > 0 && !tmp.equals(type) ? type + " " + tmp : tmp; // 兼容一天提交两次外出情况
  321. // 兼容跨天请假场景
  322. boolean sDate = date.equals(status.split(" ")[0].replace(tmp, ""));
  323. boolean eDate = date.equals(status.split(" ")[1].split("到")[1]);
  324. if (Integer.valueOf(sStart) >= 1200 && sDate) {
  325. day_2 = tmp;
  326. } else {
  327. String sEnd = status.split(" ")[2].replace(":", "");
  328. if (Integer.valueOf(sStart) < 800 || !sDate) {
  329. sStart = "0800";
  330. }
  331. float hourZao = Duration.between(UtilDateTime.parseLocal(sStart, "HHmm"), UtilDateTime.parseLocal("1200", "HHmm")).toMinutes() / 60f;
  332. if (hourZao >= 3.0f || (hourZao > 0f && tmp.equals("外出"))) {
  333. day_1 += day_1.length() > 0 ? " " + tmp : tmp;
  334. }
  335. if (Integer.valueOf(sEnd) > 1700 || !eDate) {
  336. sEnd = "1700";
  337. }
  338. float hourWan = Duration.between(UtilDateTime.parseLocal("1300", "HHmm"), UtilDateTime.parseLocal(sEnd, "HHmm")).toMinutes() / 60f;
  339. if (hourWan > 3.0f || (hourWan > 0f && tmp.equals("外出"))) {
  340. day_2 += day_2.length() > 0 ? " " + tmp : tmp;
  341. }
  342. }
  343. }
  344. } else if (status.contains("出差")) {
  345. // 出差兼容, 半天, 外出, 请假等情况
  346. type += type.length() > 0 ? (type.contains("出差") ? "" : " 出差") : "出差";
  347. // 半天出差场景以及被添加为协同人后, 钉钉也会记录一条出差; 均循环进行处理, 即时出差覆盖即当天多次出差也可兼容
  348. int sStart = Integer.valueOf(status.split(" ")[1].split("到")[0].replace(":", ""));
  349. int sEnd = Integer.valueOf(status.split(" ")[2].replace(":", ""));
  350. if (val.get("value").contains("休息")) {
  351. day_1 = day_1.equals("") ? "公假" : day_1;
  352. day_2 = day_2.equals("") ? "公假" : day_2;
  353. }
  354. if (sStart >= 1200 && date.equals(status.split(" ")[0].replace("出差", ""))) {
  355. // 跨天: 日期相等, 且下午时间
  356. day_2 = "出差";
  357. } else if (sEnd <= 1300 && date.equals(status.split(" ")[1].split("到")[1])) {
  358. // 跨天: 日期相等, 且上午时间
  359. day_1 = "出差";
  360. } else {
  361. day_1 = "出差";
  362. day_2 = "出差";
  363. }
  364. } else {
  365. /// 请假数据处理 [半天情况]
  366. String[] arr = status.split(" ");
  367. int sstart = Integer.valueOf(status.split(" ")[1].split("到")[0].replace(":", ""));
  368. float day = Float.valueOf((arr[arr.length - 1].replace("天", "")));
  369. type = tmp;
  370. if (day == 0.5) {
  371. if (sstart >= 1200) {
  372. day_2 = type;
  373. } else {
  374. day_1 = type;
  375. }
  376. } else {
  377. day_1 = type;
  378. day_2 = type;
  379. }
  380. }
  381. }
  382. }
  383. }
  384. // 日期动态列头
  385. if (!days.contains(date)) {
  386. days.add(date);
  387. }
  388. attendanceInfo.put(date, type.length() == 0 ? "zc" : type);
  389. attendanceInfo.put("day" + index + "_1", day_1.length() == 0 ? "zc" : day_1);
  390. attendanceInfo.put("day" + index + "_2", day_2.length() == 0 ? "zc" : day_2);
  391. }
  392. }
  393. });
  394. // 累计假期数据
  395. float leave_duration = 0f; // 法定假期调整时长: 分钟
  396. float leave_all = 0f; // 请假总时长: 天 [屏蔽3小时以内] - 新, 仅事假扣除出勤, 通过字段配置解决
  397. for (Map column : ddClient_attendance.getLeaveTimeByNames(ddClient.getAccessToken(), po.getUserId(), leaveNames, start, end)) {
  398. String name = ((Map) column.get("columnvo")).get("name").toString(); // 接口返回列名称
  399. float value = (Float) _reduceAttendance(column, name, "columnvals");
  400. // prd 法定假期[除病假、事件、调休、产假外]请假时长 [调休, 事假, 哺乳假为小时, 其余半天为最小单位]
  401. if (!Arrays.asList("病假", "事假", "调休", "产假").contains(name)) {
  402. if (name.equals("哺乳假")) {
  403. leave_duration += value * 60f;
  404. } else {
  405. leave_duration += value * 60f * 8f;
  406. }
  407. }
  408. // prd 病假,产假,事假扣除出勤天数. 因事假按照小时请假, 3小时内记录为出勤, 3-4小时为半天, 4小时以上记录为一天, 因此钉钉后台未设置自动扣减
  409. if (Arrays.asList("病假", "产假", "事假").contains(name)) {
  410. if (name.equals("事假")) {
  411. // 系统已自动过滤, 午休时间 [跨天场景]
  412. if (value > 8f) {
  413. leave_all += Math.floor(value / 8f);
  414. }
  415. float hours = value % 8;
  416. if (hours > 0f) {
  417. // prd 1. 3小时以下不扣除; 2. 大于等于3,小于6为半天; 3. 大于等于6为1天
  418. if (hours >= 6.0f) {
  419. leave_all += 1.0f;
  420. } else if (hours >= 3f) {
  421. leave_all += 0.5f;
  422. }
  423. }
  424. } else {
  425. leave_all += value;
  426. }
  427. }
  428. attendanceInfo.put(name, value);
  429. }
  430. // 数据处理, 请假折算天
  431. float overTime = UtilMap.getFloat(attendanceInfo, "节假日加班") + UtilMap.getFloat(attendanceInfo, "节假日加班") + UtilMap.getFloat(attendanceInfo, "节假日加班");
  432. attendanceInfo.put("加班总时长", UtilNumber.formatPrecisionValue(overTime));
  433. attendanceInfo.put("事假天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "事假") / 8f));
  434. attendanceInfo.put("哺乳假天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "哺乳假") / 8f));
  435. attendanceInfo.put("调休天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "调休") / 8f));
  436. // prd 标记人离职时间, 提示异常考勤
  437. float exception_duration = 0f;
  438. if (ObjectUtils.isNotEmpty(po.getHiredDate()) && UtilDateTime.beforeAndEqual(UtilDateTime.parseDateTime(start), po.getHiredDate()) && UtilDateTime.afterAndEqual(UtilDateTime.parseDateTime(end), po.getHiredDate())) {
  439. Optional optional = Arrays.stream(attendanceInfo.get("考勤结果").toString().split("; ")).filter(item -> item.contains("迟到") && item.contains(UtilDateTime.formatDate(po.getHiredDate()))).findAny();
  440. if (optional.isPresent()) {
  441. exception_duration = Float.valueOf((optional.get().toString().split("分钟")[0].replace("上班迟到", "").replace("上班严重迟到", "")));
  442. attendanceInfo.put("迟到次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "迟到次数") - 1));
  443. }
  444. attendanceInfo.put("考勤结果", "入职日期" + UtilDateTime.formatDate(po.getHiredDate()) + "; " + attendanceInfo.get("考勤结果"));
  445. }
  446. attendanceInfo.put("缺卡次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "上班缺卡次数") + UtilMap.getFloat(attendanceInfo, "下班缺卡次数")));
  447. attendanceInfo.put("出勤天数_sys", attendanceInfo.get("出勤天数")); // prd 离职1号计算, 请假扣除, 部分员工旷工算出勤, 扣除休息打卡出勤
  448. if (ObjectUtils.isNotEmpty(po.getLeaveDate()) && UtilDateTime.parseDateTime(start).before(po.getLeaveDate()) && UtilDateTime.parseDateTime(end).after(po.getLeaveDate())) {
  449. // prd 离职员工出勤天数是否可以只记录员工离职当月1号
  450. Optional option = attendanceList.stream().filter(item -> {
  451. /// 线程安全, 对象获取值
  452. String id = (((Map) item.get("column_vo"))).get("id").toString();
  453. return fileId_attendance_days.get().equals(id);
  454. }).findAny();
  455. if (option.isPresent()) {
  456. List<Map> dataList = (List<Map>) ((Map) option.get()).get("column_vals");
  457. for (Map data : dataList) {
  458. if (UtilDateTime.parseDate(data.get("date").toString()).getMonth() != UtilDateTime.parseDate(end).getMonth()) {
  459. log.info("离职从1号计算出勤, {}, {}, {}, {}", po.getName(), data.get("date"), UtilMap.getFloat(attendanceInfo, "出勤天数_sys"), UtilMap.getFloat(data, "value"));
  460. attendanceInfo.put("出勤天数_sys", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "出勤天数_sys") - UtilMap.getFloat(data, "value")));
  461. }
  462. }
  463. }
  464. // 缺卡补录
  465. Optional optional = Arrays.stream(attendanceInfo.get("考勤结果").toString().split("; ")).filter(item -> item.equals("下班缺卡" + UtilDateTime.formatDate(po.getLeaveDate()))).findAny();
  466. if (optional.isPresent()) {
  467. exception_duration = 480f;
  468. attendanceInfo.put("缺卡次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "缺卡次数") - 1));
  469. }
  470. attendanceInfo.put("考勤结果", "离职日期" + UtilDateTime.formatDate(po.getLeaveDate()) + "; " + attendanceInfo.get("考勤结果"));
  471. }
  472. attendanceInfo.put("缺卡调整时长", UtilNumber.formatPrecisionValue(exception_duration));
  473. // prd 总时长 = 工作时长 + 法定假期[除病假、事件、调休、产假外]请假时长 + 调休时长 - 加班时长【出差、外出不考勤但需要计入总工时,以申请时长为准,但外出可能为不足一天情况, 当天还有打卡: 目前先取系统默认】
  474. float system_duration = UtilMap.getFloat(attendanceInfo, "工作时长");
  475. float tiaoxiu_duration = UtilMap.getFloat(attendanceInfo, "调休") * 60f;
  476. attendanceInfo.put("调休时长", UtilNumber.formatPrecisionValue(tiaoxiu_duration));
  477. attendanceInfo.put("法定假调整时长", UtilNumber.formatPrecisionValue(leave_duration));
  478. // prd [新] 汇总表: 不取系统调休。总时长计算取 0,返回列表也为 0
  479. attendanceInfo.put("总时长", UtilNumber.formatPrecisionValue(system_duration + leave_duration + exception_duration - overTime));
  480. // prd 请假扣除出勤天数 ppExt 钉钉接口休息如出差半天系统也返回出勤天数1, 存在异常; 休息日加班也会记录为出勤, 考勤字段调整无效
  481. attendanceInfo.put("出勤天数_sys", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "出勤天数_sys") - leave_all));
  482. // prd 公假打卡的天数无需记录到出勤天数中, 包含出差部分
  483. Optional optional = attendanceList.stream().filter(item -> {
  484. /// 线程安全, 对象获取值
  485. String id = (((Map) item.get("column_vo"))).get("id").toString();
  486. return fileId_attendance_result.get().equals(id);
  487. }).findAny();
  488. if (optional.isPresent()) {
  489. List<Map> dataList = (List<Map>) ((Map) optional.get()).get("column_vals");
  490. int days_overTime = dataList.stream().filter(item -> String.valueOf(item.get("value")).contains("休息并打卡") || String.valueOf(item.get("value")).contains("休息,出差")).collect(Collectors.toList()).size();
  491. attendanceInfo.put("出勤天数_sys", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "出勤天数_sys") - days_overTime));
  492. }
  493. attendanceInfos.add(attendanceInfo);
  494. });
  495. // prd 26-25周期非自然月逻辑 [获取出现最多次作为法定应出勤天数] 考勤应出勤天数和班组 + 人员挂钩, ppExt 排班天数钉钉查询没有接口
  496. float workMin = (Float) UtilList.maxFrequencyObject(attendanceInfos.stream().map(item -> UtilMap.getFloat(item, "出勤天数")).collect(Collectors.toList())) * 60 * 8;
  497. // prd 数据处理 [ppExt 月度汇总统计真实数据, 月度明细按照zc规则统计]
  498. int order = 0;
  499. for (Map attendance : attendanceInfos) {
  500. if (attendance.containsKey("总时长") && workMin > 0) {
  501. attendance.put("勤勉度系数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendance, "总时长") / workMin));
  502. }
  503. order++;
  504. attendance.put("序号", String.valueOf(order));
  505. // 调休按照半天\一天进行取整, 补充尾差
  506. attendance.put("出勤天数_sys", UtilNumber.roundHalf(UtilMap.getFloat(attendance, "出勤天数_sys")));
  507. // prd 月度汇总表和月度明细表是否可实现部分无需打卡的员工
  508. if ("无需打卡".equals(attendance.get("考勤状态"))) {
  509. if (!Objects.isNull(days)) {
  510. attendance.put("旷工天数", 0);
  511. attendance.put("缺卡次数", 0);
  512. attendance.put("上班缺卡次数", 0);
  513. attendance.put("上班缺卡次数", 0);
  514. attendance.put("下班缺卡次数", 0);
  515. attendance.put("迟到次数", 0);
  516. attendance.put("早退次数", 0);
  517. for (Object key : attendance.keySet()) {
  518. String prop = String.valueOf(key);
  519. if (prop.contains("_") || prop.contains("-")) {
  520. String val = String.valueOf(attendance.get(prop)).replace("旷工", "").replace("缺卡", "").replace("迟到", "").replace("早退", "").trim();
  521. // 忽略考勤异常 | 考勤静默用户
  522. if (StringUtils.isBlank(val) || val.equals("/")) {
  523. attendance.put(prop, "zc");
  524. } else {
  525. attendance.put(prop, val);
  526. }
  527. }
  528. }
  529. } else {
  530. List<String> vals = new ArrayList<>();
  531. for (String cont : String.valueOf(attendance.get("考勤结果")).split("; ")) {
  532. // 缺卡情况下, 存在请假, 需要保留
  533. if (cont.contains("缺卡,") || (!cont.contains("旷工") && !cont.contains("缺卡") && !cont.contains("迟到") && !cont.contains("早退"))) {
  534. vals.add(cont);
  535. }
  536. }
  537. attendance.put("考勤结果", String.join("; ", vals));
  538. // prd 部分无需打卡的员工旷工、缺卡、迟到、早退的天数需要记录到出勤天数中
  539. attendance.put("出勤天数_sys", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendance, "出勤天数_sys") + UtilMap.getFloat(attendance, "旷工天数")));
  540. }
  541. }
  542. if (!Objects.isNull(days)) {
  543. // prd 异常与假期统计对应状态数据, 出勤天数就是实际到公司工作的天数[zc状态], 兼容3小时需求逻辑
  544. AtomicReference<Float> days_rest = new AtomicReference<>(0f);
  545. AtomicReference<Float> days_tiaoxiu = new AtomicReference<>(0f);
  546. AtomicReference<Float> days_shijia = new AtomicReference<>(0f);
  547. AtomicReference<Float> days_burujia = new AtomicReference<>(0f);
  548. AtomicReference<Float> days_chidao = new AtomicReference<>(0f);
  549. AtomicReference<Float> days_zaotui = new AtomicReference<>(0f);
  550. AtomicReference<Float> days_kuangong = new AtomicReference<>(0f);
  551. attendance.put("出勤天数_prd", attendance.keySet().stream().reduce(0f, (acc, cur) -> {
  552. if (cur.toString().contains("_")) {
  553. // 累计汇总天数 [出勤天数包含 外出, 缺卡, zc]; ppExt 未加入考勤组人员, 会全月统计为zc, 钉钉返回为空, 没有判断条件. 因此需要手动调整 [极端情况]
  554. if (Arrays.asList("zc", "缺卡", "外出").contains(attendance.get(cur))) {
  555. return Float.valueOf(String.valueOf(acc)) + 0.5;
  556. } else if (attendance.get(cur).equals("公假")) {
  557. days_rest.updateAndGet(v -> new Float((float) (v + 0.5)));
  558. } else if (attendance.get(cur).equals("调休")) {
  559. days_tiaoxiu.updateAndGet(v -> new Float((float) (v + 0.5)));
  560. } else if (attendance.get(cur).equals("事假")) {
  561. days_shijia.updateAndGet(v -> new Float((float) (v + 0.5)));
  562. } else if (attendance.get(cur).equals("哺乳假")) {
  563. days_burujia.updateAndGet(v -> new Float((float) (v + 0.5)));
  564. } else if (attendance.get(cur).equals("迟到")) {
  565. days_chidao.updateAndGet(v -> new Float((float) (v + 0.5)));
  566. } else if (attendance.get(cur).equals("早退")) {
  567. days_zaotui.updateAndGet(v -> new Float((float) (v + 0.5)));
  568. } else if (attendance.get(cur).equals("旷工")) {
  569. days_kuangong.updateAndGet(v -> new Float((float) (v + 0.5)));
  570. }
  571. }
  572. return acc;
  573. }));
  574. attendance.put("公假天数_prd", days_rest.get());
  575. attendance.put("调休天数_prd", days_tiaoxiu.get());
  576. attendance.put("事假天数_prd", days_shijia.get());
  577. attendance.put("哺乳假天数_prd", days_burujia.get());
  578. attendance.put("迟到次数_prd", days_chidao.get());
  579. attendance.put("早退次数_prd", days_zaotui.get());
  580. attendance.put("旷工天数_prd", days_kuangong.get());
  581. }
  582. }
  583. // 记录月度明细日期, 进行排序 [接口返回已排序]
  584. // if (UtilList.isNotEmpty(days)) {
  585. // Collections.sort(days, Comparator.comparingLong(o -> Long.valueOf(o.replace("-", ""))));
  586. // }
  587. return UtilList.ignoreListMapZero(attendanceInfos);
  588. }
  589. }