From eee03bbfea8b15d1a56ab63bd09a35c6c861c4cb Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 09 十一月 2026 13:01:08 +0800
Subject: [PATCH] feat(mes): 不合格品处置联动入库与生产订单

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java |  586 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 577 insertions(+), 9 deletions(-)

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
index 97975d7..9cc5db0 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -3,19 +3,27 @@
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEducationSaveReqVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEmergencyContactSaveReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportExcelVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeePageReqVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSimpleRespVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeStatisticsRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeWorkHistorySaveReqVO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeContractDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeEducationDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeEmergencyContactDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeWorkHistoryDO;
+import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeContractMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeEducationMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeEmergencyContactMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
@@ -29,13 +37,24 @@
 import cn.iocoder.yudao.module.system.api.permission.dto.RoleRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserUpdateReqDTO;
+import com.mzt.logapi.starter.annotation.LogRecord;
 import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -43,7 +62,9 @@
 import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
 import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.*;
 
 /**
  * 鍛樺伐淇℃伅 Service 瀹炵幇绫�
@@ -54,10 +75,15 @@
 @Validated
 public class HrmEmployeeServiceImpl implements HrmEmployeeService {
 
+    /** 鍛樺伐鐘舵�侊細绂昏亴 */
+    private static final Integer EMPLOYEE_STATUS_RESIGNED = 3;
+
     @Resource
     private HrmEmployeeMapper employeeMapper;
     @Resource
     private HrmEmployeeEducationMapper educationMapper;
+    @Resource
+    private HrmEmployeeContractMapper contractMapper;
     @Resource
     private HrmEmployeeWorkHistoryMapper workHistoryMapper;
     @Resource
@@ -72,6 +98,9 @@
     private AdminUserApi adminUserApi;
     @Resource
     private RoleApi roleApi;
+    @Resource
+    @Lazy // 鑷敞鍏ラ渶寤惰繜鍔犺浇锛屽惁鍒� bean 琚唬鐞嗗寘瑁呭悗娉ㄥ叆鍘熷鐗堟湰瀵艰嚧鍚姩澶辫触
+    private HrmEmployeeService self;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -79,7 +108,7 @@
         // 1. 鏍¢獙閮ㄩ棬
         deptApi.validateDeptList(Collections.singleton(createReqVO.getDeptId()));
         // 2. 鏍¢獙宀椾綅
-        postApi.validPostList(Collections.singleton(createReqVO.getPostId()));
+        postApi.validPostList(createReqVO.getPostIds());
         // 3. 鏍¢獙鎵嬫満鍙锋槸鍚﹀凡瀛樺湪
         if (employeeMapper.selectByPhone(createReqVO.getPhone()) != null) {
             throw exception(EMPLOYEE_PHONE_EXISTS);
@@ -89,7 +118,7 @@
         userCreateReqDTO.setUsername(createReqVO.getPhone());
         userCreateReqDTO.setNickname(createReqVO.getName());
         userCreateReqDTO.setDeptId(createReqVO.getDeptId());
-        userCreateReqDTO.setPostIds(Collections.singleton(createReqVO.getPostId()));
+        userCreateReqDTO.setPostIds(new LinkedHashSet<>(createReqVO.getPostIds()));
         userCreateReqDTO.setMobile(createReqVO.getPhone());
         userCreateReqDTO.setPassword(createReqVO.getPhone());
         userCreateReqDTO.setSex(createReqVO.getGender());
@@ -109,6 +138,10 @@
         // roleIds 瀛樺偍涓� JSON
         if (CollUtil.isNotEmpty(createReqVO.getRoleIds())) {
             employee.setRoleIds(JSONUtil.toJsonStr(createReqVO.getRoleIds()));
+        }
+        // postIds 瀛樺偍涓� JSON
+        if (CollUtil.isNotEmpty(createReqVO.getPostIds())) {
+            employee.setPostIds(JSONUtil.toJsonStr(createReqVO.getPostIds()));
         }
         employeeMapper.insert(employee);
         Long employeeId = employee.getId();
@@ -155,7 +188,7 @@
         // 鏍¢獙閮ㄩ棬
         deptApi.validateDeptList(Collections.singleton(updateReqVO.getDeptId()));
         // 鏍¢獙宀椾綅
-        postApi.validPostList(Collections.singleton(updateReqVO.getPostId()));
+        postApi.validPostList(updateReqVO.getPostIds());
         // 鏍¢獙鎵嬫満鍙锋槸鍚﹀凡瀛樺湪锛堝鏋滄墜鏈哄彿鏈夊彉鏇达級
         if (!Objects.equals(employee.getPhone(), updateReqVO.getPhone())
                 && employeeMapper.selectByPhone(updateReqVO.getPhone()) != null) {
@@ -167,7 +200,24 @@
         if (CollUtil.isNotEmpty(updateReqVO.getRoleIds())) {
             updateObj.setRoleIds(JSONUtil.toJsonStr(updateReqVO.getRoleIds()));
         }
+        // postIds 瀛樺偍涓� JSON
+        if (CollUtil.isNotEmpty(updateReqVO.getPostIds())) {
+            updateObj.setPostIds(JSONUtil.toJsonStr(updateReqVO.getPostIds()));
+        }
         employeeMapper.updateById(updateObj);
+        // 鍚屾鏇存柊鍏宠仈绯荤粺鐢ㄦ埛锛堝矖浣�/閮ㄩ棬/鏄电О/瑙掕壊/閭/澶囨敞锛夛紝淇濇寔涓庣敤鎴风鐞嗕竴鑷�
+        if (employee.getUserId() != null) {
+            AdminUserUpdateReqDTO userUpdateReqDTO = new AdminUserUpdateReqDTO();
+            userUpdateReqDTO.setId(employee.getUserId());
+            userUpdateReqDTO.setNickname(updateReqVO.getName());
+            userUpdateReqDTO.setDeptId(updateReqVO.getDeptId());
+            userUpdateReqDTO.setPostIds(new LinkedHashSet<>(updateReqVO.getPostIds()));
+            userUpdateReqDTO.setRoleIds(CollUtil.isEmpty(updateReqVO.getRoleIds()) ? null
+                    : new LinkedHashSet<>(updateReqVO.getRoleIds()));
+            userUpdateReqDTO.setEmail(updateReqVO.getEmail());
+            userUpdateReqDTO.setRemark(updateReqVO.getRemark());
+            adminUserApi.updateUser(userUpdateReqDTO);
+        }
         Long employeeId = updateReqVO.getId();
 
         // 鍚屾鏁欒偛缁忓巻
@@ -258,12 +308,16 @@
                 respVO.setDeptName(dept.getName());
             }
         }
-        // 濉厖宀椾綅鍚嶇О
-        if (employee.getPostId() != null) {
-            Map<Long, PostRespDTO> postMap = postApi.getPostMap(Collections.singleton(employee.getPostId()));
-            PostRespDTO post = postMap.get(employee.getPostId());
-            if (post != null) {
-                respVO.setPostName(post.getName());
+        // 濉厖宀椾綅淇℃伅
+        if (StrUtil.isNotBlank(employee.getPostIds())) {
+            List<Long> postIds = JSONUtil.toList(employee.getPostIds(), Long.class);
+            respVO.setPostIds(postIds);
+            if (CollUtil.isNotEmpty(postIds)) {
+                Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
+                respVO.setPostNames(postIds.stream()
+                        .filter(postMap::containsKey)
+                        .map(postId -> postMap.get(postId).getName())
+                        .collect(Collectors.joining(",")));
             }
         }
         // 濉厖瑙掕壊鍚嶇О
@@ -287,12 +341,35 @@
 
     @Override
     public PageResult<HrmEmployeeDO> getEmployeePage(HrmEmployeePageReqVO pageReqVO) {
+        if (pageReqVO.getDeptId() != null) {
+            // 閫変腑閮ㄩ棬鏃讹紝杩炲甫鏌ヨ鍏舵墍鏈夊瓙閮ㄩ棬鐨勫憳宸ワ紙鍚嚜韬級
+            List<DeptRespDTO> childDepts = deptApi.getChildDeptList(pageReqVO.getDeptId());
+            Set<Long> deptIds = new LinkedHashSet<>();
+            deptIds.add(pageReqVO.getDeptId());
+            if (CollUtil.isNotEmpty(childDepts)) {
+                childDepts.forEach(dept -> deptIds.add(dept.getId()));
+            }
+            return employeeMapper.selectPageByDeptIds(pageReqVO, deptIds);
+        }
         return employeeMapper.selectPage(pageReqVO);
     }
 
     @Override
     public List<HrmEmployeeDO> getEmployeeList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
         return employeeMapper.selectByIds(ids);
+    }
+
+    @Override
+    public List<HrmEmployeeDO> getEmployeeListByStatus(Integer employeeStatus) {
+        return employeeMapper.selectListByStatus(employeeStatus);
+    }
+
+    @Override
+    public List<HrmEmployeeDO> getEmployeeListByPostIds(Collection<Long> postIds) {
+        return employeeMapper.selectListByPostIds(postIds);
     }
 
     @Override
@@ -322,4 +399,495 @@
         return employeeMapper.selectByUserId(userId);
     }
 
+    @Override
+    public boolean updateEmployeeStatusToResignedByUserId(Long userId, LocalDate leaveDate) {
+        if (userId == null) {
+            return false;
+        }
+        return employeeMapper.updateEmployeeStatusByUserId(userId, EMPLOYEE_STATUS_RESIGNED, leaveDate) > 0;
+    }
+
+    @Override
+    public HrmEmployeeDO getEmployeeByName(String name) {
+        return employeeMapper.selectByName(name);
+    }
+
+    @Override
+    public List<HrmEmployeeDO> getEmployeeListByName(String name) {
+        return employeeMapper.selectListByName(name);
+    }
+
+    @Override
+    @LogRecord(type = HRM_EMPLOYEE_TYPE, subType = HRM_EMPLOYEE_IMPORT_SUB_TYPE, bizNo = "0",
+            success = HRM_EMPLOYEE_IMPORT_SUCCESS)
+    public HrmEmployeeImportRespVO importEmployeeList(List<HrmEmployeeImportExcelVO> importEmployees,
+                                                      HrmEmployeeImportReqVO importReqVO) {
+        // 1. 鏍¢獙闈炵┖
+        importEmployees = filterList(importEmployees, item -> Objects.nonNull(item.getName()));
+        if (CollUtil.isEmpty(importEmployees)) {
+            throw exception(EMPLOYEE_IMPORT_LIST_IS_EMPTY);
+        }
+        // 2. 棰勫姞杞介儴闂�/宀椾綅/瑙掕壊 鍚嶇О -> 瀹炰綋 鏄犲皠
+        Map<String, DeptRespDTO> deptMap = deptApi.getDeptList().stream()
+                .collect(Collectors.toMap(DeptRespDTO::getName, dept -> dept, (v1, v2) -> v1));
+        Map<String, PostRespDTO> postMap = postApi.getPostList().stream()
+                .collect(Collectors.toMap(PostRespDTO::getName, post -> post, (v1, v2) -> v1));
+        Map<String, RoleRespDTO> roleMap = roleApi.getRoleList().stream()
+                .collect(Collectors.toMap(RoleRespDTO::getName, role -> role, (v1, v2) -> v1));
+        // 3. 閫愭潯澶勭悊
+        HrmEmployeeImportRespVO respVO = HrmEmployeeImportRespVO.builder()
+                .createNames(new ArrayList<>())
+                .updateNames(new ArrayList<>())
+                .failureNames(new LinkedHashMap<>())
+                .build();
+        for (HrmEmployeeImportExcelVO importEmployee : importEmployees) {
+            String name = importEmployee.getName();
+            try {
+                // 3.1 鍚嶇О杞� ID
+                Long deptId = parseDeptId(importEmployee.getDeptName(), deptMap);
+                Set<Long> postIds = parsePostIds(importEmployee.getPostName(), postMap);
+                Set<Long> roleIds = parseRoleIds(importEmployee.getRoleNames(), roleMap);
+                // 3.2 鏋勫缓淇濆瓨璇锋眰
+                HrmEmployeeSaveReqVO saveReqVO = buildImportSaveReqVO(importEmployee, deptId, postIds, roleIds);
+                // 3.3 鍒ゆ柇瀛樺湪鎬э細鎸夋墜鏈哄彿
+                HrmEmployeeDO existEmployee = employeeMapper.selectByPhone(importEmployee.getPhone());
+                if (existEmployee == null) {
+                    self.createEmployee(saveReqVO);
+                    respVO.getCreateNames().add(name);
+                } else {
+                    if (!Boolean.TRUE.equals(importReqVO.getUpdateSupport())) {
+                        respVO.getFailureNames().put(name, EMPLOYEE_PHONE_EXISTS.getMsg());
+                        continue;
+                    }
+                    saveReqVO.setId(existEmployee.getId());
+                    self.updateEmployee(saveReqVO);
+                    respVO.getUpdateNames().add(name);
+                }
+            } catch (ServiceException ex) {
+                respVO.getFailureNames().put(name, ex.getMessage());
+            } catch (Exception ex) {
+                respVO.getFailureNames().put(name, ex.getMessage());
+            }
+        }
+        // 璁板綍瀵煎叆缁撴灉姹囨�伙紝渚� @LogRecord 鐨� success 鏂囨寮曠敤
+        OperateLogUtils.putImportSummary(respVO.getCreateNames(), respVO.getUpdateNames(),
+                respVO.getFailureNames());
+        return respVO;
+    }
+
+    private HrmEmployeeSaveReqVO buildImportSaveReqVO(HrmEmployeeImportExcelVO importEmployee,
+                                                      Long deptId, Set<Long> postIds, Set<Long> roleIds) {
+        if (StrUtil.isBlank(importEmployee.getName())) {
+            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "鍛樺伐濮撳悕");
+        }
+        if (StrUtil.isBlank(importEmployee.getPhone())) {
+            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "鎵嬫満鍙�");
+        }
+        if (importEmployee.getHireDate() == null) {
+            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "鍏ヨ亴鏃ユ湡");
+        }
+        HrmEmployeeSaveReqVO reqVO = new HrmEmployeeSaveReqVO();
+        reqVO.setName(importEmployee.getName());
+        reqVO.setGender(importEmployee.getGender() != null ? importEmployee.getGender() : 1); // 榛樿鐢�
+        reqVO.setPhone(importEmployee.getPhone());
+        reqVO.setDeptId(deptId);
+        reqVO.setPostIds(CollUtil.isEmpty(postIds) ? null : new ArrayList<>(postIds));
+        reqVO.setRoleIds(CollUtil.isEmpty(roleIds) ? null : new ArrayList<>(roleIds));
+        reqVO.setHireDate(importEmployee.getHireDate());
+        reqVO.setEmployeeStatus(importEmployee.getEmployeeStatus() != null
+                ? importEmployee.getEmployeeStatus() : 1); // 榛樿鍦ㄨ亴
+        reqVO.setEmail(importEmployee.getEmail());
+        reqVO.setBirthday(importEmployee.getBirthday());
+        reqVO.setIdCard(importEmployee.getIdCard());
+        reqVO.setNation(importEmployee.getNation());
+        reqVO.setMarriageStatus(importEmployee.getMarriageStatus());
+        reqVO.setAge(importEmployee.getAge());
+        reqVO.setEducation(importEmployee.getEducation());
+        reqVO.setPoliticalStatus(importEmployee.getPoliticalStatus());
+        reqVO.setRemark(importEmployee.getRemark());
+        return reqVO;
+    }
+
+    private Long parseDeptId(String deptName, Map<String, DeptRespDTO> deptMap) {
+        if (StrUtil.isBlank(deptName)) {
+            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "閮ㄩ棬鍚嶇О");
+        }
+        DeptRespDTO dept = deptMap.get(deptName.trim());
+        if (dept == null) {
+            throw exception(EMPLOYEE_DEPT_NAME_NOT_EXISTS, deptName.trim());
+        }
+        return dept.getId();
+    }
+
+    private Set<Long> parsePostIds(String postNames, Map<String, PostRespDTO> postMap) {
+        if (StrUtil.isBlank(postNames)) {
+            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "宀椾綅鍚嶇О");
+        }
+        Set<Long> postIds = new LinkedHashSet<>();
+        for (String postName : postNames.split("[,锛宂")) {
+            String trimmed = postName.trim();
+            if (StrUtil.isBlank(trimmed)) {
+                continue;
+            }
+            PostRespDTO post = postMap.get(trimmed);
+            if (post == null) {
+                throw exception(EMPLOYEE_POST_NAME_NOT_EXISTS, trimmed);
+            }
+            postIds.add(post.getId());
+        }
+        if (CollUtil.isEmpty(postIds)) {
+            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "宀椾綅鍚嶇О");
+        }
+        return postIds;
+    }
+
+    private Set<Long> parseRoleIds(String roleNames, Map<String, RoleRespDTO> roleMap) {
+        if (StrUtil.isBlank(roleNames)) {
+            return Collections.emptySet();
+        }
+        Set<Long> roleIds = new LinkedHashSet<>();
+        for (String roleName : roleNames.split("[,锛宂")) {
+            String trimmed = roleName.trim();
+            if (StrUtil.isBlank(trimmed)) {
+                continue;
+            }
+            RoleRespDTO role = roleMap.get(trimmed);
+            if (role == null) {
+                throw exception(EMPLOYEE_ROLE_NAME_NOT_EXISTS, trimmed);
+            }
+            roleIds.add(role.getId());
+        }
+        return roleIds;
+    }
+
+    // ==================== 浜哄姏缁熻 ====================
+
+    /** 鍛樺伐鐘舵�侊細鍦ㄨ亴 */
+    private static final Integer EMPLOYEE_STATUS_ACTIVE = 1;
+    /** 鍛樺伐鐘舵�侊細璇曠敤 */
+    private static final Integer EMPLOYEE_STATUS_PROBATION = 2;
+
+    /** 宸ラ緞鍖洪棿鍚嶇О锛屼笌涓嬮檺锛堝惈锛変竴涓�瀵瑰簲锛屾渶鍚庝竴妗f棤涓婇檺 */
+    private static final String[] TENURE_BUCKET_NAMES = {"1骞翠互鍐�", "1-3骞�", "3-5骞�", "5-10骞�", "10骞翠互涓�"};
+    private static final double[] TENURE_BUCKET_MIN = {0, 1, 3, 5, 10};
+
+    /** 骞撮緞鍖洪棿鍚嶇О锛屼笌涓嬮檺锛堝惈锛変竴涓�瀵瑰簲锛屾渶鍚庝竴妗f棤涓婇檺 */
+    private static final String[] AGE_BUCKET_NAMES = {"25宀佷互涓�", "25-30宀�", "30-35宀�", "35-40宀�", "40-50宀�", "50宀佷互涓�"};
+    private static final int[] AGE_BUCKET_MIN = {0, 25, 30, 35, 40, 50};
+
+    @Override
+    public HrmEmployeeStatisticsRespVO getTenureStatistics() {
+        List<HrmEmployeeDO> allEmployees = employeeMapper.selectList();
+        LocalDate today = LocalDate.now();
+        int currentYear = today.getYear();
+
+        List<HrmEmployeeDO> activeEmployees = allEmployees.stream()
+                .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_ACTIVE)
+                        || Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_PROBATION))
+                .collect(Collectors.toList());
+
+        HrmEmployeeStatisticsRespVO respVO = new HrmEmployeeStatisticsRespVO();
+        respVO.setTotalCount(activeEmployees.size());
+        respVO.setActiveCount((int) activeEmployees.stream()
+                .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_ACTIVE)).count());
+        respVO.setProbationCount((int) activeEmployees.stream()
+                .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_PROBATION)).count());
+        respVO.setResignedCount((int) allEmployees.stream()
+                .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED)).count());
+        // 鏈勾鍏ヨ亴锛氬湪宀楀憳宸ヤ腑鍏ヨ亴鏃ユ湡钀藉湪浠婂勾
+        respVO.setThisYearHireCount((int) activeEmployees.stream()
+                .filter(e -> e.getHireDate() != null && e.getHireDate().getYear() == currentYear).count());
+        // 鏈勾绂昏亴锛氱鑱屽憳宸ヤ腑绂昏亴鏃ユ湡钀藉湪浠婂勾
+        respVO.setThisYearResignCount((int) allEmployees.stream()
+                .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED))
+                .filter(e -> e.getLeaveDate() != null && e.getLeaveDate().getYear() == currentYear).count());
+
+        // 1. 骞冲潎宸ラ緞銆佸钩鍧囧勾榫�
+        List<Double> tenureList = activeEmployees.stream()
+                .map(e -> calculateTenureYears(e, today))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        respVO.setAverageTenureYears(averageOf(tenureList));
+        List<Integer> ageList = activeEmployees.stream()
+                .map(e -> resolveAge(e, today))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        respVO.setAverageAge(averageOfInt(ageList));
+
+        // 2. 宸ラ緞鍒嗗竷
+        int[] tenureCounts = new int[TENURE_BUCKET_NAMES.length];
+        for (Double tenure : tenureList) {
+            tenureCounts[resolveTenureBucket(tenure)]++;
+        }
+        List<HrmEmployeeStatisticsRespVO.ItemVO> tenureDistribution = new ArrayList<>();
+        for (int i = 0; i < TENURE_BUCKET_NAMES.length; i++) {
+            tenureDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO(TENURE_BUCKET_NAMES[i], tenureCounts[i]));
+        }
+        respVO.setTenureDistribution(tenureDistribution);
+
+        // 3. 骞撮緞鍒嗗竷
+        int[] ageCounts = new int[AGE_BUCKET_NAMES.length];
+        for (Integer age : ageList) {
+            ageCounts[resolveAgeBucket(age)]++;
+        }
+        List<HrmEmployeeStatisticsRespVO.ItemVO> ageDistribution = new ArrayList<>();
+        for (int i = 0; i < AGE_BUCKET_NAMES.length; i++) {
+            ageDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO(AGE_BUCKET_NAMES[i], ageCounts[i]));
+        }
+        respVO.setAgeDistribution(ageDistribution);
+
+        // 4. 瀛﹀巻鍒嗗竷锛堟湭濉啓鐨勫綊鍏モ�滄湭濉啓鈥濓級
+        Map<String, Integer> educationMap = new LinkedHashMap<>();
+        activeEmployees.stream()
+                .map(e -> StrUtil.blankToDefault(StrUtil.trim(e.getEducation()), "鏈~鍐�"))
+                .forEach(name -> educationMap.merge(name, 1, Integer::sum));
+        respVO.setEducationDistribution(toItemListSorted(educationMap));
+
+        // 5. 鎬у埆鍒嗗竷锛�1 鐢� / 2 濂� / 鍏跺畠 鏈煡锛�
+        Map<String, Integer> genderMap = new LinkedHashMap<>();
+        activeEmployees.forEach(e -> {
+            String name = Objects.equals(e.getGender(), 1) ? "鐢�" : Objects.equals(e.getGender(), 2) ? "濂�" : "鏈煡";
+            genderMap.merge(name, 1, Integer::sum);
+        });
+        respVO.setGenderDistribution(toItemListSorted(genderMap));
+
+        // 6. 閮ㄩ棬宸ラ緞缁熻
+        Set<Long> deptIds = activeEmployees.stream()
+                .map(HrmEmployeeDO::getDeptId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Long, DeptRespDTO> deptMap = CollUtil.isEmpty(deptIds) ? Collections.emptyMap() : deptApi.getDeptMap(deptIds);
+        Map<Long, List<Double>> deptTenureMap = new LinkedHashMap<>();
+        Map<Long, String> deptNameMap = new LinkedHashMap<>();
+        for (HrmEmployeeDO employee : activeEmployees) {
+            Double tenure = calculateTenureYears(employee, today);
+            if (employee.getDeptId() == null) {
+                deptTenureMap.computeIfAbsent(0L, k -> new ArrayList<>()).add(tenure == null ? 0D : tenure);
+                deptNameMap.putIfAbsent(0L, "鏈垎閰嶉儴闂�");
+                continue;
+            }
+            deptTenureMap.computeIfAbsent(employee.getDeptId(), k -> new ArrayList<>())
+                    .add(tenure == null ? 0D : tenure);
+            DeptRespDTO dept = deptMap.get(employee.getDeptId());
+            deptNameMap.put(employee.getDeptId(), dept != null ? dept.getName() : "閮ㄩ棬ID=" + employee.getDeptId());
+        }
+        List<HrmEmployeeStatisticsRespVO.DeptTenureVO> deptTenureList = new ArrayList<>();
+        deptTenureMap.forEach((deptId, tenures) -> {
+            HrmEmployeeStatisticsRespVO.DeptTenureVO deptVO = new HrmEmployeeStatisticsRespVO.DeptTenureVO();
+            deptVO.setDeptId(deptId == 0L ? null : deptId);
+            deptVO.setDeptName(deptNameMap.get(deptId));
+            deptVO.setCount(tenures.size());
+            deptVO.setAverageTenureYears(averageOf(tenures));
+            deptTenureList.add(deptVO);
+        });
+        deptTenureList.sort(Comparator.comparing(HrmEmployeeStatisticsRespVO.DeptTenureVO::getAverageTenureYears,
+                Comparator.nullsLast(Comparator.reverseOrder())));
+        respVO.setDeptTenureList(deptTenureList);
+
+        // 6.1 閮ㄩ棬浜烘暟鍒嗗竷锛堜笌閮ㄩ棬宸ラ緞鍚屼竴浠藉垎缁勶紝鐩存帴澶嶇敤锛�
+        Map<String, Integer> deptCountMap = new LinkedHashMap<>();
+        deptTenureMap.forEach((deptId, tenures) -> deptCountMap.put(deptNameMap.get(deptId), tenures.size()));
+        respVO.setDeptDistribution(toItemListSorted(deptCountMap));
+
+        // 7. 宸ラ緞鎺掕锛堝墠 10 鍚嶏級
+        respVO.setTopTenureEmployees(activeEmployees.stream()
+                .filter(e -> e.getHireDate() != null)
+                .sorted(Comparator.comparing(HrmEmployeeDO::getHireDate))
+                .limit(10)
+                .map(e -> {
+                    HrmEmployeeStatisticsRespVO.TenureEmployeeVO employeeVO = new HrmEmployeeStatisticsRespVO.TenureEmployeeVO();
+                    employeeVO.setId(e.getId());
+                    employeeVO.setName(e.getName());
+                    DeptRespDTO dept = e.getDeptId() == null ? null : deptMap.get(e.getDeptId());
+                    employeeVO.setDeptName(dept != null ? dept.getName() : (e.getDeptId() == null ? "鏈垎閰嶉儴闂�" : "閮ㄩ棬ID=" + e.getDeptId()));
+                    employeeVO.setHireDate(e.getHireDate());
+                    employeeVO.setTenureYears(calculateTenureYears(e, today));
+                    return employeeVO;
+                })
+                .collect(Collectors.toList()));
+
+        // 8. 浜哄憳鐘舵�佹瀯鎴愶紙鍦ㄨ亴 / 璇曠敤 / 绂昏亴锛屽叾浣欑姸鎬佸綊鍏モ�滃叾瀹冣�濓級
+        int activeStatusCount = (int) activeEmployees.stream()
+                .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_ACTIVE)).count();
+        int probationStatusCount = (int) activeEmployees.stream()
+                .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_PROBATION)).count();
+        int resignedStatusCount = (int) allEmployees.stream()
+                .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED)).count();
+        List<HrmEmployeeStatisticsRespVO.ItemVO> statusDistribution = new ArrayList<>();
+        statusDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO("鍦ㄨ亴", activeStatusCount));
+        statusDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO("璇曠敤", probationStatusCount));
+        statusDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO("绂昏亴", resignedStatusCount));
+        int otherStatusCount = allEmployees.size() - activeStatusCount - probationStatusCount - resignedStatusCount;
+        if (otherStatusCount > 0) {
+            statusDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO("鍏跺畠", otherStatusCount));
+        }
+        respVO.setStatusDistribution(statusDistribution);
+
+        // 9. 宀椾綅鍒嗗竷锛坧ostIds 涓� JSON 鏁扮粍锛屽彲涓�浜哄宀楋紱鏃犲矖浣嶅綊鍏モ�滄湭鍒嗛厤宀椾綅鈥濓級
+        Set<Long> postIds = new HashSet<>();
+        for (HrmEmployeeDO employee : activeEmployees) {
+            if (StrUtil.isNotBlank(employee.getPostIds())) {
+                postIds.addAll(JSONUtil.toList(employee.getPostIds(), Long.class));
+            }
+        }
+        Map<Long, PostRespDTO> postMap = CollUtil.isEmpty(postIds) ? Collections.emptyMap() : postApi.getPostMap(postIds);
+        Map<String, Integer> postCountMap = new LinkedHashMap<>();
+        for (HrmEmployeeDO employee : activeEmployees) {
+            List<Long> employeePostIds = StrUtil.isNotBlank(employee.getPostIds())
+                    ? JSONUtil.toList(employee.getPostIds(), Long.class) : Collections.emptyList();
+            if (CollUtil.isEmpty(employeePostIds)) {
+                postCountMap.merge("鏈垎閰嶅矖浣�", 1, Integer::sum);
+                continue;
+            }
+            for (Long postId : employeePostIds) {
+                PostRespDTO post = postMap.get(postId);
+                postCountMap.merge(post != null ? post.getName() : "宀椾綅ID=" + postId, 1, Integer::sum);
+            }
+        }
+        respVO.setPostDistribution(toItemListSorted(postCountMap));
+
+        // 10. 濠氬Щ鐘跺喌銆佹斂娌婚潰璨屽垎甯冿紙妗f鑷敱鏂囨湰锛岀┖鍊煎綊鍏モ�滄湭濉啓鈥濓級
+        Map<String, Integer> marriageMap = new LinkedHashMap<>();
+        Map<String, Integer> politicalMap = new LinkedHashMap<>();
+        for (HrmEmployeeDO employee : activeEmployees) {
+            marriageMap.merge(StrUtil.blankToDefault(StrUtil.trim(employee.getMarriageStatus()), "鏈~鍐�"), 1, Integer::sum);
+            politicalMap.merge(StrUtil.blankToDefault(StrUtil.trim(employee.getPoliticalStatus()), "鏈~鍐�"), 1, Integer::sum);
+        }
+        respVO.setMarriageDistribution(toItemListSorted(marriageMap));
+        respVO.setPoliticalDistribution(toItemListSorted(politicalMap));
+
+        // 11. 杩� 12 涓湀鍏ヨ亴 / 绂昏亴瓒嬪娍锛堝惈鏈湀锛�
+        Map<YearMonth, Integer> hireMonthMap = new LinkedHashMap<>();
+        Map<YearMonth, Integer> resignMonthMap = new LinkedHashMap<>();
+        allEmployees.forEach(e -> {
+            if (e.getHireDate() != null) {
+                hireMonthMap.merge(YearMonth.from(e.getHireDate()), 1, Integer::sum);
+            }
+            if (e.getLeaveDate() != null) {
+                resignMonthMap.merge(YearMonth.from(e.getLeaveDate()), 1, Integer::sum);
+            }
+        });
+        List<HrmEmployeeStatisticsRespVO.TrendVO> trendList = new ArrayList<>();
+        YearMonth startMonth = YearMonth.from(today).minusMonths(11);
+        for (int i = 0; i < 12; i++) {
+            YearMonth month = startMonth.plusMonths(i);
+            HrmEmployeeStatisticsRespVO.TrendVO trendVO = new HrmEmployeeStatisticsRespVO.TrendVO();
+            trendVO.setMonth(month.toString());
+            trendVO.setHireCount(hireMonthMap.getOrDefault(month, 0));
+            trendVO.setResignCount(resignMonthMap.getOrDefault(month, 0));
+            trendList.add(trendVO);
+        }
+        respVO.setHireResignTrend(trendList);
+
+        // 12. 鍚堝悓鍒版湡棰勮锛堝彧鐪嬪湪宀楀憳宸ョ殑褰撳墠鐢熸晥鍚堝悓锛�
+        Set<Long> activeEmployeeIds = activeEmployees.stream()
+                .map(HrmEmployeeDO::getId)
+                .collect(Collectors.toSet());
+        Map<Long, HrmEmployeeContractDO> currentContractMap = contractMapper.selectList().stream()
+                .filter(c -> Boolean.TRUE.equals(c.getIsCurrent()))
+                .filter(c -> c.getTerminateStatus() == null || c.getTerminateStatus() == 0)
+                .filter(c -> activeEmployeeIds.contains(c.getEmployeeId()))
+                .collect(Collectors.toMap(HrmEmployeeContractDO::getEmployeeId, c -> c, (a, b) -> a));
+        int expiredCount = 0;
+        int expireIn30Count = 0;
+        int expireIn90Count = 0;
+        int expireOver90Count = 0;
+        int unlimitedCount = 0;
+        for (HrmEmployeeContractDO contract : currentContractMap.values()) {
+            if (contract.getEndDate() == null) {
+                unlimitedCount++;
+                continue;
+            }
+            long days = ChronoUnit.DAYS.between(today, contract.getEndDate());
+            if (days < 0) {
+                expiredCount++;
+            } else if (days <= 30) {
+                expireIn30Count++;
+            } else if (days <= 90) {
+                expireIn90Count++;
+            } else {
+                expireOver90Count++;
+            }
+        }
+        List<HrmEmployeeStatisticsRespVO.ItemVO> contractWarning = new ArrayList<>();
+        contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("宸插埌鏈�", expiredCount));
+        contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("30澶╁唴鍒版湡", expireIn30Count));
+        contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("31-90澶╁埌鏈�", expireIn90Count));
+        contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("90澶╀互涓�", expireOver90Count));
+        contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("鏃犲浐瀹氭湡闄�", unlimitedCount));
+        respVO.setContractWarning(contractWarning);
+        respVO.setNoContractCount(Math.max(0, activeEmployeeIds.size() - currentContractMap.size()));
+
+        return respVO;
+    }
+
+    /**
+     * 璁$畻宸ラ緞锛堝勾锛屼繚鐣� 1 浣嶅皬鏁帮級锛涚鑱屽憳宸ユ寜绂昏亴鏃ユ湡璁$畻锛屾棤鍏ヨ亴鏃ユ湡杩斿洖 null
+     */
+    private Double calculateTenureYears(HrmEmployeeDO employee, LocalDate today) {
+        if (employee.getHireDate() == null) {
+            return null;
+        }
+        LocalDate endDate = employee.getLeaveDate() != null ? employee.getLeaveDate() : today;
+        if (endDate.isBefore(employee.getHireDate())) {
+            return 0D;
+        }
+        double years = ChronoUnit.DAYS.between(employee.getHireDate(), endDate) / 365.25D;
+        return Math.round(years * 10D) / 10D;
+    }
+
+    /**
+     * 瑙f瀽骞撮緞锛氫紭鍏堝彇妗f骞撮緞锛岀己澶辨椂鐢ㄥ嚭鐢熸棩鏈熸帹绠�
+     */
+    private Integer resolveAge(HrmEmployeeDO employee, LocalDate today) {
+        if (employee.getAge() != null && employee.getAge() > 0) {
+            return employee.getAge();
+        }
+        if (employee.getBirthday() == null) {
+            return null;
+        }
+        return (int) ChronoUnit.YEARS.between(employee.getBirthday(), today);
+    }
+
+    private int resolveTenureBucket(double tenure) {
+        for (int i = TENURE_BUCKET_MIN.length - 1; i >= 0; i--) {
+            if (tenure >= TENURE_BUCKET_MIN[i]) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+    private int resolveAgeBucket(int age) {
+        for (int i = AGE_BUCKET_MIN.length - 1; i >= 0; i--) {
+            if (age >= AGE_BUCKET_MIN[i]) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+    private List<HrmEmployeeStatisticsRespVO.ItemVO> toItemListSorted(Map<String, Integer> countMap) {
+        return countMap.entrySet().stream()
+                .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
+                .map(entry -> new HrmEmployeeStatisticsRespVO.ItemVO(entry.getKey(), entry.getValue()))
+                .collect(Collectors.toList());
+    }
+
+    private Double averageOf(List<Double> values) {
+        if (CollUtil.isEmpty(values)) {
+            return 0D;
+        }
+        double sum = values.stream().mapToDouble(Double::doubleValue).sum();
+        return Math.round(sum / values.size() * 10D) / 10D;
+    }
+
+    private Double averageOfInt(List<Integer> values) {
+        if (CollUtil.isEmpty(values)) {
+            return 0D;
+        }
+        double sum = values.stream().mapToInt(Integer::intValue).sum();
+        return Math.round(sum / values.size() * 10D) / 10D;
+    }
+
 }
\ No newline at end of file

--
Gitblit v1.9.3