| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package com.ruoyi.staff.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.fasterxml.jackson.databind.JsonNode; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.ruoyi.approve.mapper.ApprovalInstanceMapper; |
| | | import com.ruoyi.approve.pojo.ApprovalInstance; |
| | | import com.ruoyi.common.enums.TypeEnums; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.project.system.domain.SysUser; |
| | | import com.ruoyi.project.system.mapper.SysUserMapper; |
| | | import com.ruoyi.staff.dto.SalaryStat; |
| | | import com.ruoyi.staff.mapper.PersonalAttendanceRecordsMapper; |
| | | import com.ruoyi.staff.pojo.PersonalAttendanceRecords; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.YearMonth; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * åå·¥å½æè¯·åæ¬¡æ°ãç¼ºå¤æ¬¡æ°ãè¿å°æ¬¡æ°ãæ©é次æ°ç»è®¡ |
| | | * <p> |
| | | * è¯·åæ°æ®ååå审æ¹ä¸ç请åç³è¯·ï¼approval_instance.business_type 为 2ã请å管çãæ 14ã请å审æ¹ãï¼ |
| | | * 审æ¹ç¶æä¸ºå·²éè¿ï¼ï¼æè¯·åæ¶é´ï¼è¡¨åä¸çå¼å§/ç»ææ¶é´ï¼ä¸å·¥èµæä»½æ¯å¦éå 夿å½å±æä»½ï¼ |
| | | * èæ°æ®æ²¡æè¡¨åæ¶é´æ¶éåæç³è¯·æ¶é´å¤æï¼ |
| | | * 缺å¤ãè¿å°ãæ©éæ°æ®å个人èå¤è®°å½è¡¨ personal_attendance_records ç status åæ®µ |
| | | * ï¼0æ£å¸¸ 1è¿å° 2æ©é 3è¿å°æ©é 4缺å¤ï¼ï¼ç³»ç»å®æ¶ä»»å¡æ¯å¤©åæ¨ä¼ç»æ²¡ææå¡çå¨èåå·¥èªå¨è¡¥ä¸æ¡ç¼ºå¤è®°å½ã |
| | | * </p> |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class SalaryStatServiceImpl { |
| | | |
| | | /** èå¤ç¶æï¼è¿å° */ |
| | | private static final int ATTENDANCE_STATUS_LATE = 1; |
| | | /** èå¤ç¶æï¼æ©é */ |
| | | private static final int ATTENDANCE_STATUS_EARLY = 2; |
| | | /** èå¤ç¶æï¼è¿å°æ©é */ |
| | | private static final int ATTENDANCE_STATUS_LATE_AND_EARLY = 3; |
| | | /** èå¤ç¶æï¼ç¼ºå¤ï¼å®æ¶ä»»å¡ä¸ºæªæå¡åå·¥èªå¨çæï¼ */ |
| | | private static final int ATTENDANCE_STATUS_ABSENCE = 4; |
| | | /** åä¸å工彿夿¬¡è¯·åæ¶ï¼è¯·ååå æ¼æ¥åé符 */ |
| | | private static final String LEAVE_REASON_SEPARATOR = "ï¼"; |
| | | /** staff_salary_detail.remark åæ®µé¿åº¦ï¼è¶
é¿æªæï¼é¿å
ååºæ¥é */ |
| | | private static final int REMARK_MAX_LENGTH = 200; |
| | | /** 请å审æ¹å·²éè¿ç¶æ */ |
| | | private static final String LEAVE_STATUS_APPROVED = "APPROVED"; |
| | | /** 审æ¹é»è¾å é¤ï¼æªå é¤ */ |
| | | private static final Byte NOT_DELETED = (byte) 0; |
| | | private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); |
| | | /** 表åä¸ä¸éè¦æ¼æ¥è¿è¯·ååå çåæ®µå
³é®åï¼æ¶é´èå´ãéä»¶çï¼ */ |
| | | private static final String[] IGNORE_LABEL_KEYWORDS = {"æ¶é´", "æ¥æ", "ç³è¯·äºº", "é¨é¨", "éä»¶", "å¾ç", "ä¸ä¼ "}; |
| | | /** 请ååå åæ®µå
³é®åï¼å½ä¸åä¼å
ä½ä¸ºè¯·ååå */ |
| | | private static final String[] REASON_LABEL_KEYWORDS = {"åå ", "äºç±", "çç±", "说æ"}; |
| | | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| | | |
| | | private final PersonalAttendanceRecordsMapper personalAttendanceRecordsMapper; |
| | | private final ApprovalInstanceMapper approvalInstanceMapper; |
| | | private final SysUserMapper sysUserMapper; |
| | | |
| | | /** |
| | | * ç»è®¡åå·¥æå®æä»½çè¯·åæ¬¡æ°ãç¼ºå¤æ¬¡æ°ãè¿å°æ¬¡æ°ãæ©é次æ°å请ååå |
| | | * |
| | | * @param staffOnJobId staff_on_job è¡¨ä¸»é® |
| | | * @param staffName åå·¥å§åï¼ç¨äºå¹é
审æ¹ç³è¯·äººï¼ |
| | | * @param salaryMonth å·¥èµæä»½ï¼æ ¼å¼ yyyy-MM |
| | | * @return ç»è®¡ç»æï¼åæ°ä¸åæ³æ¶è¿å次æ°ä¸º 0 çç»æ |
| | | */ |
| | | public SalaryStat stat(Long staffOnJobId, String staffName, String salaryMonth) { |
| | | SalaryStat stat = new SalaryStat(); |
| | | stat.setLeaveCount(0); |
| | | stat.setAbsenceCount(0); |
| | | stat.setLateCount(0); |
| | | stat.setEarlyCount(0); |
| | | stat.setLeaveReason(null); |
| | | YearMonth month = parseMonth(salaryMonth); |
| | | if (staffOnJobId == null || month == null) { |
| | | return stat; |
| | | } |
| | | fillAttendanceStat(stat, staffOnJobId, month); |
| | | List<ApprovalInstance> leaveList = listLeaveInstances(staffName, month); |
| | | stat.setLeaveCount(leaveList.size()); |
| | | stat.setLeaveReason(joinLeaveReason(leaveList)); |
| | | return stat; |
| | | } |
| | | |
| | | /** |
| | | * èå¤ç»è®¡ï¼ç¼ºå¤æ¬¡æ°ãè¿å°æ¬¡æ°ãæ©é次æ°ï¼è¿å°æ©éåæ¶è®¡å
¥ä¸¤è¾¹ï¼ |
| | | */ |
| | | private void fillAttendanceStat(SalaryStat stat, Long staffOnJobId, YearMonth month) { |
| | | List<PersonalAttendanceRecords> records = personalAttendanceRecordsMapper.selectList( |
| | | new LambdaQueryWrapper<PersonalAttendanceRecords>() |
| | | .eq(PersonalAttendanceRecords::getStaffOnJobId, staffOnJobId) |
| | | .ge(PersonalAttendanceRecords::getDate, month.atDay(1)) |
| | | .le(PersonalAttendanceRecords::getDate, month.atEndOfMonth())); |
| | | int absenceCount = 0; |
| | | int lateCount = 0; |
| | | int earlyCount = 0; |
| | | for (PersonalAttendanceRecords record : records) { |
| | | Integer status = record.getStatus(); |
| | | if (status == null) { |
| | | continue; |
| | | } |
| | | if (status == ATTENDANCE_STATUS_ABSENCE) { |
| | | absenceCount++; |
| | | } else if (status == ATTENDANCE_STATUS_LATE) { |
| | | lateCount++; |
| | | } else if (status == ATTENDANCE_STATUS_EARLY) { |
| | | earlyCount++; |
| | | } else if (status == ATTENDANCE_STATUS_LATE_AND_EARLY) { |
| | | lateCount++; |
| | | earlyCount++; |
| | | } |
| | | } |
| | | stat.setAbsenceCount(absenceCount); |
| | | stat.setLateCount(lateCount); |
| | | stat.setEarlyCount(earlyCount); |
| | | } |
| | | |
| | | /** |
| | | * æ¥è¯¢åå·¥å½å±å½æçå·²éè¿è¯·åç³è¯·ï¼æè¯·åæ¶é´ååº |
| | | * <p> |
| | | * å
æç³è¯·äººæ¥åºææå·²éè¿ç请åï¼è¯·å管çã请å审æ¹ä¸¤ç§ä¸å¡ç±»åé½å
¼å®¹ï¼ï¼ |
| | | * åæè¯·åæ¶é´ä¸å·¥èµæä»½æ¯å¦éå è¿æ»¤ï¼è·¨æè¯·åï¼å¦ä¸æ27æ¥å°æ¬æ2æ¥ï¼å¨ä¸¤ä¸ªæçå·¥èµè¡¨éé½è½ç»è®¡å°ã |
| | | * èæ°æ®ç审æ¹è¡¨åæ²¡ææ¶é´å段æ¶ï¼éåæç³è¯·æ¶é´å¤ææå±æä»½ã |
| | | * </p> |
| | | */ |
| | | private List<ApprovalInstance> listLeaveInstances(String staffName, YearMonth month) { |
| | | List<Long> applicantIds = resolveApplicantIds(staffName); |
| | | if (applicantIds.isEmpty() && StringUtils.isEmpty(staffName)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | LambdaQueryWrapper<ApprovalInstance> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.in(ApprovalInstance::getBusinessType, |
| | | TypeEnums.LEAVE.getCode(), TypeEnums.LEAVE_APPROVAL.getCode()) |
| | | .eq(ApprovalInstance::getStatus, LEAVE_STATUS_APPROVED) |
| | | .eq(ApprovalInstance::getDeleted, NOT_DELETED) |
| | | .orderByAsc(ApprovalInstance::getApplyTime); |
| | | if (applicantIds.isEmpty()) { |
| | | wrapper.eq(ApprovalInstance::getApplicantName, staffName); |
| | | } else { |
| | | String name = staffName; |
| | | wrapper.and(item -> item.in(ApprovalInstance::getApplicantId, applicantIds) |
| | | .or().eq(ApprovalInstance::getApplicantName, name)); |
| | | } |
| | | List<ApprovalInstance> allList = approvalInstanceMapper.selectList(wrapper); |
| | | |
| | | List<ApprovalInstance> monthList = new ArrayList<>(); |
| | | for (ApprovalInstance instance : allList) { |
| | | if (matchMonth(instance, month)) { |
| | | monthList.add(instance); |
| | | } |
| | | } |
| | | return monthList; |
| | | } |
| | | |
| | | /** |
| | | * å¤æè¯·åæ¯å¦å±äºå½æï¼ä¼å
çè¯·åæ¶é´ä¸å½ææ¯å¦éå ï¼æ²¡æè¯·åæ¶é´æ¶çç³è¯·æ¶é´ |
| | | */ |
| | | private boolean matchMonth(ApprovalInstance instance, YearMonth month) { |
| | | List<LocalDate> leaveDates = extractLeaveDates(instance.getFormConfig()); |
| | | if (!leaveDates.isEmpty()) { |
| | | LocalDate begin = Collections.min(leaveDates); |
| | | LocalDate end = Collections.max(leaveDates); |
| | | return !end.isBefore(month.atDay(1)) && !begin.isAfter(month.atEndOfMonth()); |
| | | } |
| | | if (instance.getApplyTime() == null) { |
| | | return false; |
| | | } |
| | | return YearMonth.from(instance.getApplyTime()).equals(month); |
| | | } |
| | | |
| | | /** |
| | | * ä»å®¡æ¹è¡¨åçå¡«æ¥å¼éååºè¯·åå¼å§/ç»ææ¶é´ |
| | | */ |
| | | private List<LocalDate> extractLeaveDates(String formConfig) { |
| | | if (StringUtils.isEmpty(formConfig)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<LocalDate> dates = new ArrayList<>(); |
| | | try { |
| | | JsonNode payload = OBJECT_MAPPER.readTree(formConfig).path("formPayload"); |
| | | if (!payload.isObject()) { |
| | | return dates; |
| | | } |
| | | payload.fields().forEachRemaining(entry -> collectDates(entry.getValue(), dates)); |
| | | } catch (Exception e) { |
| | | log.warn("è§£æè¯·å审æ¹è¡¨åæ¶é´å¤±è´¥ï¼{}", e.getMessage()); |
| | | } |
| | | return dates; |
| | | } |
| | | |
| | | private void collectDates(JsonNode node, List<LocalDate> dates) { |
| | | if (node == null || node.isNull() || node.isMissingNode()) { |
| | | return; |
| | | } |
| | | if (node.isArray()) { |
| | | for (JsonNode item : node) { |
| | | collectDates(item, dates); |
| | | } |
| | | return; |
| | | } |
| | | LocalDate date = parseDate(node.asText("")); |
| | | if (date != null) { |
| | | dates.add(date); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * è§£æ yyyy-MM-dd / yyyy-MM-dd HH:mm:ss / yyyy/M/d çæ ¼å¼çæ¥æ |
| | | */ |
| | | private LocalDate parseDate(String text) { |
| | | if (StringUtils.isEmpty(text)) { |
| | | return null; |
| | | } |
| | | String value = text.trim().replace('/', '-'); |
| | | if (value.length() < 8) { |
| | | return null; |
| | | } |
| | | String head = value.length() > 10 ? value.substring(0, 10) : value; |
| | | String[] parts = head.split("-"); |
| | | if (parts.length != 3) { |
| | | return null; |
| | | } |
| | | try { |
| | | int year = Integer.parseInt(parts[0]); |
| | | int month = Integer.parseInt(parts[1]); |
| | | int day = Integer.parseInt(parts[2]); |
| | | return LocalDate.of(year, month, day); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * éè¿åå·¥å§åå¹é
ç³»ç»ç¨æ·ï¼å®¡æ¹ç³è¯·äºº = ç³»ç»ç¨æ·ï¼ |
| | | */ |
| | | private List<Long> resolveApplicantIds(String staffName) { |
| | | if (StringUtils.isEmpty(staffName)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<SysUser> userList = sysUserMapper.selectList(new LambdaQueryWrapper<SysUser>() |
| | | .eq(SysUser::getUserName, staffName) |
| | | .eq(SysUser::getDelFlag, "0")); |
| | | List<Long> userIds = new ArrayList<>(); |
| | | for (SysUser user : userList) { |
| | | if (user.getUserId() != null) { |
| | | userIds.add(user.getUserId()); |
| | | } |
| | | } |
| | | return userIds; |
| | | } |
| | | |
| | | /** |
| | | * æ¼æ¥è¯·ååå ï¼å¤æ¬¡è¯·åç¨ï¼åå²ï¼å¹¶å»é |
| | | */ |
| | | private String joinLeaveReason(List<ApprovalInstance> leaveList) { |
| | | Set<String> reasonSet = new LinkedHashSet<>(); |
| | | for (ApprovalInstance instance : leaveList) { |
| | | String reason = extractLeaveReason(instance); |
| | | if (StringUtils.isNotEmpty(reason)) { |
| | | reasonSet.add(reason); |
| | | } |
| | | } |
| | | if (reasonSet.isEmpty()) { |
| | | return null; |
| | | } |
| | | String joined = String.join(LEAVE_REASON_SEPARATOR, reasonSet); |
| | | if (joined.length() > REMARK_MAX_LENGTH) { |
| | | joined = joined.substring(0, REMARK_MAX_LENGTH - 1) + "â¦"; |
| | | } |
| | | return joined; |
| | | } |
| | | |
| | | /** |
| | | * ä»å®¡æ¹è¡¨å䏿å请ååå ï¼ |
| | | * 1. ä¼å
å表åéãåå /äºç±/çç±/说æãç±»åæ®µï¼ |
| | | * 2. 没æåå åæ®µæ¶ï¼å表åé餿¶é´ãç³è¯·äººãé件以å¤çææ¬åæ®µï¼ |
| | | * 3. 表å为空ï¼èæ°æ®ï¼æ¶ï¼éåå®¡æ¹æ é¢ã |
| | | */ |
| | | private String extractLeaveReason(ApprovalInstance instance) { |
| | | String formConfig = instance.getFormConfig(); |
| | | if (StringUtils.isNotEmpty(formConfig)) { |
| | | try { |
| | | JsonNode root = OBJECT_MAPPER.readTree(formConfig); |
| | | JsonNode payload = root.path("formPayload"); |
| | | if (payload.isObject() && payload.size() > 0) { |
| | | JsonNode fields = root.path("fields"); |
| | | List<String> reasonValues = new ArrayList<>(); |
| | | List<String> otherValues = new ArrayList<>(); |
| | | if (fields.isArray()) { |
| | | for (JsonNode field : fields) { |
| | | String key = field.path("key").asText(""); |
| | | if (StringUtils.isEmpty(key) || !payload.has(key)) { |
| | | continue; |
| | | } |
| | | String label = field.path("label").asText(""); |
| | | String type = field.path("type").asText(""); |
| | | String value = nodeText(payload.get(key)); |
| | | if (StringUtils.isEmpty(value) || isIgnoredField(label, type)) { |
| | | continue; |
| | | } |
| | | if (isReasonField(label)) { |
| | | reasonValues.add(value); |
| | | } else { |
| | | otherValues.add(value); |
| | | } |
| | | } |
| | | } |
| | | List<String> values = reasonValues.isEmpty() ? otherValues : reasonValues; |
| | | String joined = String.join("ï¼", values); |
| | | if (StringUtils.isNotEmpty(joined)) { |
| | | return joined; |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("è§£æè¯·å审æ¹è¡¨å失败ï¼instanceId={}ï¼åå ï¼{}", instance.getId(), e.getMessage()); |
| | | } |
| | | } |
| | | return cleanTitle(instance.getTitle()); |
| | | } |
| | | |
| | | /** |
| | | * 表åå¼è½¬ææ¬ï¼æ°ç»ç¨ãæ¼æ¥ |
| | | */ |
| | | private String nodeText(JsonNode node) { |
| | | if (node == null || node.isNull() || node.isMissingNode()) { |
| | | return null; |
| | | } |
| | | if (node.isArray()) { |
| | | List<String> texts = new ArrayList<>(); |
| | | for (JsonNode item : node) { |
| | | String text = nodeText(item); |
| | | if (StringUtils.isNotEmpty(text)) { |
| | | texts.add(text); |
| | | } |
| | | } |
| | | return texts.isEmpty() ? null : String.join("ã", texts); |
| | | } |
| | | String text = node.asText(""); |
| | | if (StringUtils.isEmpty(text) || "null".equals(text)) { |
| | | return null; |
| | | } |
| | | return text.trim(); |
| | | } |
| | | |
| | | private boolean isIgnoredField(String label, String type) { |
| | | String typeText = type == null ? "" : type.toLowerCase(); |
| | | if (typeText.contains("date") || typeText.contains("time") || typeText.contains("file") |
| | | || typeText.contains("image") || typeText.contains("upload")) { |
| | | return true; |
| | | } |
| | | return containsAny(label, IGNORE_LABEL_KEYWORDS); |
| | | } |
| | | |
| | | private boolean isReasonField(String label) { |
| | | return containsAny(label, REASON_LABEL_KEYWORDS); |
| | | } |
| | | |
| | | private boolean containsAny(String text, String[] keywords) { |
| | | if (StringUtils.isEmpty(text)) { |
| | | return false; |
| | | } |
| | | for (String keyword : keywords) { |
| | | if (text.contains(keyword)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * å®¡æ¹æ é¢å»æã请å审æ¹ãåç¼ä½ä¸ºåå å
åºï¼å¦ã请å审æ¹-è°ä¼ 1 天ãâãè°ä¼ 1 天ã |
| | | */ |
| | | private String cleanTitle(String title) { |
| | | if (StringUtils.isEmpty(title)) { |
| | | return null; |
| | | } |
| | | String text = title.trim(); |
| | | String[] prefixes = {"请å审æ¹-", "请å审æ¹", "请åç³è¯·-", "请åç³è¯·", "请å-"}; |
| | | for (String prefix : prefixes) { |
| | | if (text.startsWith(prefix)) { |
| | | text = text.substring(prefix.length()).trim(); |
| | | break; |
| | | } |
| | | } |
| | | return text.isEmpty() ? null : text; |
| | | } |
| | | |
| | | private YearMonth parseMonth(String salaryMonth) { |
| | | if (StringUtils.isEmpty(salaryMonth)) { |
| | | return null; |
| | | } |
| | | String text = salaryMonth.trim(); |
| | | if (text.length() > 7) { |
| | | text = text.substring(0, 7); |
| | | } |
| | | try { |
| | | return YearMonth.parse(text, MONTH_FORMATTER); |
| | | } catch (Exception e) { |
| | | log.warn("å·¥èµæä»½æ ¼å¼ä¸æ¯ yyyy-MMï¼æ æ³ç»è®¡è¯·å/è夿¬¡æ°ï¼{}", salaryMonth); |
| | | return null; |
| | | } |
| | | } |
| | | } |