diff --git a/.gitignore b/.gitignore index 9327d07..b012e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ target/ .project .settings .springBeans +.claude ### IntelliJ IDEA ### .idea diff --git a/doc/test-data/employee/employee_test_data_1000.xlsx b/doc/test-data/employee/employee_test_data_1000.xlsx index 6a966ac..9c43d57 100644 Binary files a/doc/test-data/employee/employee_test_data_1000.xlsx and b/doc/test-data/employee/employee_test_data_1000.xlsx differ diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java index d2a3455..8498b13 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java @@ -9,6 +9,7 @@ import com.ruoyi.ccdi.domain.vo.CcdiEmployeeVO; import com.ruoyi.ccdi.domain.vo.ImportFailureVO; import com.ruoyi.ccdi.domain.vo.ImportResultVO; import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.service.ICcdiEmployeeImportService; import com.ruoyi.ccdi.service.ICcdiEmployeeService; import com.ruoyi.ccdi.utils.EasyExcelUtil; import com.ruoyi.common.annotation.Log; @@ -43,6 +44,9 @@ public class CcdiEmployeeController extends BaseController { @Resource private ICcdiEmployeeService employeeService; + @Resource + private ICcdiEmployeeImportService importAsyncService; + /** * 查询员工列表 */ @@ -137,10 +141,11 @@ public class CcdiEmployeeController extends BaseController { } // 提交异步任务 - employeeService.importEmployeeAsync(list, updateSupport); + String taskId = employeeService.importEmployee(list, updateSupport); // 立即返回,不等待后台任务完成 ImportResultVO result = new ImportResultVO(); + result.setTaskId(taskId); result.setStatus("PROCESSING"); result.setMessage("导入任务已提交,正在后台处理"); @@ -155,7 +160,7 @@ public class CcdiEmployeeController extends BaseController { @GetMapping("/importStatus/{taskId}") public AjaxResult getImportStatus(@PathVariable String taskId) { try { - ImportStatusVO status = employeeService.getImportStatus(taskId); + ImportStatusVO status = importAsyncService.getImportStatus(taskId); return success(status); } catch (Exception e) { return error(e.getMessage()); @@ -173,7 +178,7 @@ public class CcdiEmployeeController extends BaseController { @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { - List failures = employeeService.getImportFailures(taskId); + List failures = importAsyncService.getImportFailures( taskId); // 手动分页 int fromIndex = (pageNum - 1) * pageSize; diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportResult.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportResult.java new file mode 100644 index 0000000..36b6171 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportResult.java @@ -0,0 +1,15 @@ +package com.ruoyi.ccdi.domain.vo; + +import lombok.Data; + +/** + * @Author: wkc + * @CreateTime: 2026-02-06 + */ +@Data +public class ImportResult { + + private Integer totalCount; + private Integer successCount; + private Integer failureCount; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java index 725059a..2afc15a 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java @@ -27,21 +27,8 @@ public interface CcdiEmployeeMapper extends BaseMapper { Page selectEmployeePageWithDept(@Param("page") Page page, @Param("query") CcdiEmployeeQueryDTO queryDTO); - /** - * 查询员工详情(包含亲属列表) - * - * @param employeeId 员工ID - * @return 员工VO - */ - CcdiEmployeeVO selectEmployeeWithRelatives(@Param("employeeId") Long employeeId); - /** - * 批量插入或更新员工信息 - * 使用ON DUPLICATE KEY UPDATE语法 - * - * @param list 员工信息列表 - * @return 影响行数 - */ + int insertOrUpdateBatch(@Param("list") List list); /** diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeImportService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeImportService.java new file mode 100644 index 0000000..acce4c4 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeImportService.java @@ -0,0 +1,40 @@ +package com.ruoyi.ccdi.service; + +import com.ruoyi.ccdi.domain.excel.CcdiEmployeeExcel; +import com.ruoyi.ccdi.domain.vo.ImportFailureVO; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; + +import java.util.List; + +/** + * @Author: wkc + * @CreateTime: 2026-02-06 + */ +public interface ICcdiEmployeeImportService { + + + + /** + * 异步导入员工数据 + * + * @param excelList Excel数据列表 + * @param isUpdateSupport 是否更新已存在的数据 + */ + void importEmployeeAsync(List excelList, Boolean isUpdateSupport, String taskId); + + /** + * 查询导入状态 + * + * @param taskId 任务ID + * @return 导入状态信息 + */ + ImportStatusVO getImportStatus(String taskId); + + /** + * 获取导入失败记录 + * + * @param taskId 任务ID + * @return 失败记录列表 + */ + List getImportFailures(String taskId); +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeService.java index baaca93..a2cf196 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeService.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeService.java @@ -6,12 +6,8 @@ import com.ruoyi.ccdi.domain.dto.CcdiEmployeeEditDTO; import com.ruoyi.ccdi.domain.dto.CcdiEmployeeQueryDTO; import com.ruoyi.ccdi.domain.excel.CcdiEmployeeExcel; import com.ruoyi.ccdi.domain.vo.CcdiEmployeeVO; -import com.ruoyi.ccdi.domain.vo.ImportFailureVO; -import com.ruoyi.ccdi.domain.vo.ImportResultVO; -import com.ruoyi.ccdi.domain.vo.ImportStatusVO; import java.util.List; -import java.util.concurrent.CompletableFuture; /** * 员工信息 服务层 @@ -87,27 +83,4 @@ public interface ICcdiEmployeeService { */ String importEmployee(List excelList, Boolean isUpdateSupport); - /** - * 异步导入员工数据 - * - * @param excelList Excel数据列表 - * @param isUpdateSupport 是否更新已存在的数据 - */ - void importEmployeeAsync(List excelList, Boolean isUpdateSupport); - - /** - * 查询导入状态 - * - * @param taskId 任务ID - * @return 导入状态信息 - */ - ImportStatusVO getImportStatus(String taskId); - - /** - * 获取导入失败记录 - * - * @param taskId 任务ID - * @return 失败记录列表 - */ - List getImportFailures(String taskId); } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeImportServiceImpl.java new file mode 100644 index 0000000..e5003a4 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeImportServiceImpl.java @@ -0,0 +1,277 @@ +package com.ruoyi.ccdi.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.ccdi.domain.CcdiEmployee; +import com.ruoyi.ccdi.domain.dto.CcdiEmployeeAddDTO; +import com.ruoyi.ccdi.domain.excel.CcdiEmployeeExcel; +import com.ruoyi.ccdi.domain.vo.ImportFailureVO; +import com.ruoyi.ccdi.domain.vo.ImportResult; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.mapper.CcdiEmployeeMapper; +import com.ruoyi.ccdi.service.ICcdiEmployeeImportService; +import com.ruoyi.common.utils.IdCardUtil; +import com.ruoyi.common.utils.StringUtils; +import jakarta.annotation.Resource; +import org.springframework.beans.BeanUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: wkc + * @CreateTime: 2026-02-06 + */ +@Service +@EnableAsync +public class CcdiEmployeeImportServiceImpl implements ICcdiEmployeeImportService { + + @Resource + private CcdiEmployeeMapper employeeMapper; + + + @Resource + private RedisTemplate redisTemplate; + + @Override + @Async + public void importEmployeeAsync(List excelList, Boolean isUpdateSupport, String taskId) { + List newRecords = new ArrayList<>(); + List updateRecords = new ArrayList<>(); + List failures = new ArrayList<>(); + + // 批量查询已存在的柜员号 + Set existingIds = getExistingEmployeeIds(excelList); + + // 分类数据 + for (int i = 0; i < excelList.size(); i++) { + CcdiEmployeeExcel excel = excelList.get(i); + + try { + // 转换为AddDTO进行验证 + CcdiEmployeeAddDTO addDTO = new CcdiEmployeeAddDTO(); + BeanUtils.copyProperties(excel, addDTO); + + // 验证数据(支持更新模式) + validateEmployeeData(addDTO, isUpdateSupport, existingIds); + + CcdiEmployee employee = new CcdiEmployee(); + BeanUtils.copyProperties(excel, employee); + + if (existingIds.contains(excel.getEmployeeId())) { + if (isUpdateSupport) { + updateRecords.add(employee); + } else { + throw new RuntimeException("柜员号已存在且未启用更新支持"); + } + } else { + newRecords.add(employee); + } + + + } catch (Exception e) { + ImportFailureVO failure = new ImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(e.getMessage()); + failures.add(failure); + } + } + + // 批量插入新数据 + if (!newRecords.isEmpty()) { + saveBatch(newRecords, 500); + } + + // 批量更新已有数据(先删除再插入) + if (!updateRecords.isEmpty() && isUpdateSupport) { + employeeMapper.insertOrUpdateBatch(updateRecords); + } + + // 保存失败记录到Redis + if (!failures.isEmpty()) { + String failuresKey = "import:employee:" + taskId + ":failures"; + redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); + } + + ImportResult result = new ImportResult(); + result.setTotalCount(excelList.size()); + result.setSuccessCount(newRecords.size() + updateRecords.size()); + result.setFailureCount(failures.size()); + + // 更新最终状态 + String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; + updateImportStatus("employee", taskId, finalStatus, result); + } + + /** + * 获取导入失败记录 + * + * @param taskId 任务ID + * @return 失败记录列表 + */ + @Override + public List getImportFailures( String taskId) { + String key = "import:employee:" + taskId + ":failures"; + Object failuresObj = redisTemplate.opsForValue().get(key); + + if (failuresObj == null) { + return Collections.emptyList(); + } + + return JSON.parseArray(JSON.toJSONString(failuresObj), ImportFailureVO.class); + } + + /** + * 查询导入状态 + * + * @param taskId 任务ID + * @return 导入状态信息 + */ + @Override + public ImportStatusVO getImportStatus(String taskId) { + String key = "import:employee:" + taskId; + Boolean hasKey = redisTemplate.hasKey(key); + + if (Boolean.FALSE.equals(hasKey)) { + throw new RuntimeException("任务不存在或已过期"); + } + + Map statusMap = redisTemplate.opsForHash().entries(key); + + ImportStatusVO statusVO = new ImportStatusVO(); + statusVO.setTaskId((String) statusMap.get("taskId")); + statusVO.setStatus((String) statusMap.get("status")); + statusVO.setTotalCount((Integer) statusMap.get("totalCount")); + statusVO.setSuccessCount((Integer) statusMap.get("successCount")); + statusVO.setFailureCount((Integer) statusMap.get("failureCount")); + statusVO.setProgress((Integer) statusMap.get("progress")); + statusVO.setStartTime((Long) statusMap.get("startTime")); + statusVO.setEndTime((Long) statusMap.get("endTime")); + statusVO.setMessage((String) statusMap.get("message")); + + return statusVO; + } + + /** + * 更新导入状态 + */ + private void updateImportStatus(String taskType, String taskId, String status, ImportResult result) { + String key = "import:employee:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("status", status); + statusData.put("successCount", result.getSuccessCount()); + statusData.put("failureCount", result.getFailureCount()); + statusData.put("progress", 100); + statusData.put("endTime", System.currentTimeMillis()); + + if ("SUCCESS".equals(status)) { + statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据"); + } else { + statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条"); + } + + redisTemplate.opsForHash().putAll(key, statusData); + } + + /** + * 批量查询已存在的员工ID + */ + private Set getExistingEmployeeIds(List excelList) { + List employeeIds = excelList.stream() + .map(CcdiEmployeeExcel::getEmployeeId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (employeeIds.isEmpty()) { + return Collections.emptySet(); + } + + List existingEmployees = employeeMapper.selectBatchIds(employeeIds); + return existingEmployees.stream() + .map(CcdiEmployee::getEmployeeId) + .collect(Collectors.toSet()); + } + + + /** + * 批量保存 + */ + private void saveBatch(List list, int batchSize) { + // 使用真正的批量插入,分批次执行以提高性能 + for (int i = 0; i < list.size(); i += batchSize) { + int end = Math.min(i + batchSize, list.size()); + List subList = list.subList(i, end); + employeeMapper.insertBatch(subList); + } + } + + /** + * 验证员工数据 + * + * @param addDTO 新增DTO + * @param isUpdateSupport 是否支持更新 + * @param existingIds 已存在的员工ID集合(导入场景使用,传null表示单条新增) + */ + public void validateEmployeeData(CcdiEmployeeAddDTO addDTO, Boolean isUpdateSupport, Set existingIds) { + // 验证必填字段 + if (StringUtils.isEmpty(addDTO.getName())) { + throw new RuntimeException("姓名不能为空"); + } + if (addDTO.getEmployeeId() == null) { + throw new RuntimeException("柜员号不能为空"); + } + if (addDTO.getDeptId() == null) { + throw new RuntimeException("所属部门不能为空"); + } + if (StringUtils.isEmpty(addDTO.getIdCard())) { + throw new RuntimeException("身份证号不能为空"); + } + if (StringUtils.isEmpty(addDTO.getPhone())) { + throw new RuntimeException("电话不能为空"); + } + if (StringUtils.isEmpty(addDTO.getStatus())) { + throw new RuntimeException("状态不能为空"); + } + + // 验证身份证号格式 + String idCardError = IdCardUtil.getErrorMessage(addDTO.getIdCard()); + if (idCardError != null) { + throw new RuntimeException(idCardError); + } + + // 单条新增场景:检查柜员号和身份证号唯一性 + if (existingIds == null) { + // 检查柜员号(employeeId)唯一性 + if (employeeMapper.selectById(addDTO.getEmployeeId()) != null) { + throw new RuntimeException("该柜员号已存在"); + } + + // 检查身份证号唯一性 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CcdiEmployee::getIdCard, addDTO.getIdCard()); + if (employeeMapper.selectCount(wrapper) > 0) { + throw new RuntimeException("该身份证号已存在"); + } + } else { + // 导入场景:如果柜员号不存在,才检查身份证号唯一性 + if (!existingIds.contains(addDTO.getEmployeeId())) { + // 检查身份证号唯一性 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CcdiEmployee::getIdCard, addDTO.getIdCard()); + if (employeeMapper.selectCount(wrapper) > 0) { + throw new RuntimeException("该身份证号已存在"); + } + } + } + + // 验证状态 + if (!"0".equals(addDTO.getStatus()) && !"1".equals(addDTO.getStatus())) { + throw new RuntimeException("状态只能填写'在职'或'离职'"); + } + } +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java index 2c9536b..47348a5 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java @@ -1,6 +1,5 @@ package com.ruoyi.ccdi.service.impl; -import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.domain.CcdiEmployee; @@ -9,13 +8,10 @@ import com.ruoyi.ccdi.domain.dto.CcdiEmployeeEditDTO; import com.ruoyi.ccdi.domain.dto.CcdiEmployeeQueryDTO; import com.ruoyi.ccdi.domain.excel.CcdiEmployeeExcel; import com.ruoyi.ccdi.domain.vo.CcdiEmployeeVO; -import com.ruoyi.ccdi.domain.vo.ImportFailureVO; -import com.ruoyi.ccdi.domain.vo.ImportResultVO; -import com.ruoyi.ccdi.domain.vo.ImportStatusVO; import com.ruoyi.ccdi.enums.EmployeeStatus; import com.ruoyi.ccdi.mapper.CcdiEmployeeMapper; +import com.ruoyi.ccdi.service.ICcdiEmployeeImportService; import com.ruoyi.ccdi.service.ICcdiEmployeeService; -import com.ruoyi.common.utils.IdCardUtil; import com.ruoyi.common.utils.StringUtils; import jakarta.annotation.Resource; import org.springframework.beans.BeanUtils; @@ -24,10 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; /** * 员工信息 服务层处理 @@ -41,11 +34,12 @@ public class CcdiEmployeeServiceImpl implements ICcdiEmployeeService { @Resource private CcdiEmployeeMapper employeeMapper; - @Resource - private RedisTemplate redisTemplate; @Resource - private Executor importExecutor; + private ICcdiEmployeeImportService importAsyncService; + + @Resource + private RedisTemplate redisTemplate; /** * 查询员工列表 @@ -190,54 +184,6 @@ public class CcdiEmployeeServiceImpl implements ICcdiEmployeeService { @Override @Transactional public String importEmployee(List excelList, Boolean isUpdateSupport) { - if (StringUtils.isNull(excelList) || excelList.isEmpty()) { - return "至少需要一条数据"; - } - - int successNum = 0; - int failureNum = 0; - StringBuilder successMsg = new StringBuilder(); - StringBuilder failureMsg = new StringBuilder(); - - for (CcdiEmployeeExcel excel : excelList) { - try { - // 转换为AddDTO - CcdiEmployeeAddDTO addDTO = new CcdiEmployeeAddDTO(); - BeanUtils.copyProperties(excel, addDTO); - - // 验证数据 - validateEmployeeData(addDTO, isUpdateSupport, null); - - CcdiEmployee employee = new CcdiEmployee(); - BeanUtils.copyProperties(addDTO, employee); - - employeeMapper.insert(employee); - successNum++; - successMsg.append("
").append(successNum).append("、").append(addDTO.getName()).append(" 导入成功"); - } catch (Exception e) { - failureNum++; - failureMsg.append("
").append(failureNum).append("、").append(excel.getName()).append(" 导入失败:"); - failureMsg.append(e.getMessage()); - } - } - - if (failureNum > 0) { - failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); - throw new RuntimeException(failureMsg.toString()); - } else { - successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条"); - return successMsg.toString(); - } - } - - /** - * 异步导入员工数据 - * - * @param excelList Excel数据列表 - * @param isUpdateSupport 是否更新已存在的数据 - */ - @Override - public void importEmployeeAsync(List excelList, Boolean isUpdateSupport) { String taskId = UUID.randomUUID().toString(); long startTime = System.currentTimeMillis(); @@ -256,75 +202,12 @@ public class CcdiEmployeeServiceImpl implements ICcdiEmployeeService { redisTemplate.opsForHash().putAll(statusKey, statusData); redisTemplate.expire(statusKey, 7, TimeUnit.DAYS); - // 在独立线程中异步执行导入,立即返回 - CompletableFuture.runAsync(() -> { - try { - // 在后台线程中执行导入 - ImportResult result = doImport(excelList, isUpdateSupport, taskId); + importAsyncService.importEmployeeAsync(excelList, isUpdateSupport, taskId); - // 更新最终状态 - String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; - updateImportStatus(taskId, finalStatus, result, startTime); - - } catch (Exception e) { - // 处理异常 - Map errorData = new HashMap<>(); - errorData.put("status", "FAILED"); - errorData.put("message", "导入失败: " + e.getMessage()); - errorData.put("endTime", System.currentTimeMillis()); - redisTemplate.opsForHash().putAll(statusKey, errorData); - } - }, importExecutor); + return taskId; } - /** - * 查询导入状态 - * - * @param taskId 任务ID - * @return 导入状态信息 - */ - @Override - public ImportStatusVO getImportStatus(String taskId) { - String key = "import:employee:" + taskId; - Boolean hasKey = redisTemplate.hasKey(key); - if (Boolean.FALSE.equals(hasKey)) { - throw new RuntimeException("任务不存在或已过期"); - } - - Map statusMap = redisTemplate.opsForHash().entries(key); - - ImportStatusVO statusVO = new ImportStatusVO(); - statusVO.setTaskId((String) statusMap.get("taskId")); - statusVO.setStatus((String) statusMap.get("status")); - statusVO.setTotalCount((Integer) statusMap.get("totalCount")); - statusVO.setSuccessCount((Integer) statusMap.get("successCount")); - statusVO.setFailureCount((Integer) statusMap.get("failureCount")); - statusVO.setProgress((Integer) statusMap.get("progress")); - statusVO.setStartTime((Long) statusMap.get("startTime")); - statusVO.setEndTime((Long) statusMap.get("endTime")); - statusVO.setMessage((String) statusMap.get("message")); - - return statusVO; - } - - /** - * 获取导入失败记录 - * - * @param taskId 任务ID - * @return 失败记录列表 - */ - @Override - public List getImportFailures(String taskId) { - String key = "import:employee:" + taskId + ":failures"; - Object failuresObj = redisTemplate.opsForValue().get(key); - - if (failuresObj == null) { - return Collections.emptyList(); - } - - return JSON.parseArray(JSON.toJSONString(failuresObj), ImportFailureVO.class); - } /** * 构建查询条件 @@ -340,75 +223,12 @@ public class CcdiEmployeeServiceImpl implements ICcdiEmployeeService { return wrapper; } - /** - * 验证员工数据 - * - * @param addDTO 新增DTO - * @param isUpdateSupport 是否支持更新 - * @param existingIds 已存在的员工ID集合(导入场景使用,传null表示单条新增) - */ - private void validateEmployeeData(CcdiEmployeeAddDTO addDTO, Boolean isUpdateSupport, Set existingIds) { - // 验证必填字段 - if (StringUtils.isEmpty(addDTO.getName())) { - throw new RuntimeException("姓名不能为空"); - } - if (addDTO.getEmployeeId() == null) { - throw new RuntimeException("柜员号不能为空"); - } - if (addDTO.getDeptId() == null) { - throw new RuntimeException("所属部门不能为空"); - } - if (StringUtils.isEmpty(addDTO.getIdCard())) { - throw new RuntimeException("身份证号不能为空"); - } - if (StringUtils.isEmpty(addDTO.getPhone())) { - throw new RuntimeException("电话不能为空"); - } - if (StringUtils.isEmpty(addDTO.getStatus())) { - throw new RuntimeException("状态不能为空"); - } - // 验证身份证号格式 - String idCardError = IdCardUtil.getErrorMessage(addDTO.getIdCard()); - if (idCardError != null) { - throw new RuntimeException(idCardError); - } - - // 单条新增场景:检查柜员号和身份证号唯一性 - if (existingIds == null) { - // 检查柜员号(employeeId)唯一性 - if (employeeMapper.selectById(addDTO.getEmployeeId()) != null) { - throw new RuntimeException("该柜员号已存在"); - } - - // 检查身份证号唯一性 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CcdiEmployee::getIdCard, addDTO.getIdCard()); - if (employeeMapper.selectCount(wrapper) > 0) { - throw new RuntimeException("该身份证号已存在"); - } - } else { - // 导入场景:如果柜员号不存在,才检查身份证号唯一性 - if (!existingIds.contains(addDTO.getEmployeeId())) { - // 检查身份证号唯一性 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CcdiEmployee::getIdCard, addDTO.getIdCard()); - if (employeeMapper.selectCount(wrapper) > 0) { - throw new RuntimeException("该身份证号已存在"); - } - } - } - - // 验证状态 - if (!"0".equals(addDTO.getStatus()) && !"1".equals(addDTO.getStatus())) { - throw new RuntimeException("状态只能填写'在职'或'离职'"); - } - } /** * 转换为VO对象 */ - private CcdiEmployeeVO convertToVO(CcdiEmployee employee) { + public CcdiEmployeeVO convertToVO(CcdiEmployee employee) { if (employee == null) { return null; } @@ -419,177 +239,11 @@ public class CcdiEmployeeServiceImpl implements ICcdiEmployeeService { return vo; } - /** - * 执行导入逻辑 - */ - @Transactional - private ImportResult doImport(List excelList, Boolean isUpdateSupport, String taskId) { - List newRecords = new ArrayList<>(); - List updateRecords = new ArrayList<>(); - List failures = new ArrayList<>(); - // 批量查询已存在的柜员号 - Set existingIds = getExistingEmployeeIds(excelList); - // 分类数据 - for (int i = 0; i < excelList.size(); i++) { - CcdiEmployeeExcel excel = excelList.get(i); - try { - // 转换为AddDTO进行验证 - CcdiEmployeeAddDTO addDTO = new CcdiEmployeeAddDTO(); - BeanUtils.copyProperties(excel, addDTO); - // 验证数据(支持更新模式) - validateEmployeeData(addDTO, isUpdateSupport, existingIds); - CcdiEmployee employee = convertToEntity(excel); - if (existingIds.contains(excel.getEmployeeId())) { - if (isUpdateSupport) { - updateRecords.add(employee); - } else { - throw new RuntimeException("柜员号已存在且未启用更新支持"); - } - } else { - newRecords.add(employee); - } - // 更新进度 - int progress = (int) ((i + 1) * 100.0 / excelList.size()); - updateImportProgress(taskId, progress); - - } catch (Exception e) { - ImportFailureVO failure = new ImportFailureVO(); - BeanUtils.copyProperties(excel, failure); - failure.setErrorMessage(e.getMessage()); - failures.add(failure); - } - } - - // 批量插入新数据 - if (!newRecords.isEmpty()) { - saveBatch(newRecords, 500); - } - - // 批量更新已有数据 - if (!updateRecords.isEmpty() && isUpdateSupport) { - employeeMapper.insertOrUpdateBatch(updateRecords); - } - - // 保存失败记录到Redis - if (!failures.isEmpty()) { - String failuresKey = "import:employee:" + taskId + ":failures"; - redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); - } - - ImportResult result = new ImportResult(); - result.setTotalCount(excelList.size()); - result.setSuccessCount(newRecords.size() + updateRecords.size()); - result.setFailureCount(failures.size()); - - return result; - } - - /** - * 批量查询已存在的员工ID - */ - private Set getExistingEmployeeIds(List excelList) { - List employeeIds = excelList.stream() - .map(CcdiEmployeeExcel::getEmployeeId) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - if (employeeIds.isEmpty()) { - return Collections.emptySet(); - } - - List existingEmployees = employeeMapper.selectBatchIds(employeeIds); - return existingEmployees.stream() - .map(CcdiEmployee::getEmployeeId) - .collect(Collectors.toSet()); - } - - /** - * 转换为实体对象 - */ - private CcdiEmployee convertToEntity(CcdiEmployeeExcel excel) { - CcdiEmployee employee = new CcdiEmployee(); - BeanUtils.copyProperties(excel, employee); - return employee; - } - - /** - * 批量保存 - */ - private void saveBatch(List list, int batchSize) { - // 使用真正的批量插入,分批次执行以提高性能 - for (int i = 0; i < list.size(); i += batchSize) { - int end = Math.min(i + batchSize, list.size()); - List subList = list.subList(i, end); - employeeMapper.insertBatch(subList); - } - } - - /** - * 更新导入进度 - */ - private void updateImportProgress(String taskId, Integer progress) { - String key = "import:employee:" + taskId; - redisTemplate.opsForHash().put(key, "progress", progress); - } - - /** - * 更新导入状态 - */ - private void updateImportStatus(String taskId, String status, ImportResult result, Long startTime) { - String key = "import:employee:" + taskId; - Map statusData = new HashMap<>(); - statusData.put("status", status); - statusData.put("successCount", result.getSuccessCount()); - statusData.put("failureCount", result.getFailureCount()); - statusData.put("progress", 100); - statusData.put("endTime", System.currentTimeMillis()); - - if ("SUCCESS".equals(status)) { - statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据"); - } else { - statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条"); - } - - redisTemplate.opsForHash().putAll(key, statusData); - } - - /** - * 导入结果内部类 - */ - private static class ImportResult { - private Integer totalCount; - private Integer successCount; - private Integer failureCount; - - public Integer getTotalCount() { - return totalCount; - } - - public void setTotalCount(Integer totalCount) { - this.totalCount = totalCount; - } - - public Integer getSuccessCount() { - return successCount; - } - - public void setSuccessCount(Integer successCount) { - this.successCount = successCount; - } - - public Integer getFailureCount() { - return failureCount; - } - - public void setFailureCount(Integer failureCount) { - this.failureCount = failureCount; - } - } } diff --git a/ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml b/ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml index ce31d9f..a3d9917 100644 --- a/ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml +++ b/ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml @@ -44,7 +44,7 @@ ORDER BY e.create_time DESC - + INSERT INTO ccdi_employee (employee_id, name, dept_id, id_card, phone, hire_date, status, @@ -56,13 +56,12 @@ #{item.createBy}, #{item.updateBy}, NOW()) ON DUPLICATE KEY UPDATE - name = VALUES(name), - dept_id = VALUES(dept_id), - id_card = VALUES(id_card), - phone = VALUES(phone), - hire_date = VALUES(hire_date), - status = VALUES(status), - update_by = VALUES(update_by), + name = COALESCE(VALUES(name), name), + dept_id = COALESCE(VALUES(dept_id), dept_id), + phone = COALESCE(VALUES(phone), phone), + hire_date = COALESCE(VALUES(hire_date), hire_date), + status = COALESCE(VALUES(status), status), + update_by = COALESCE(VALUES(update_by), update_by), update_time = NOW()