diff --git a/doc/plans/2026-02-09-remove-import-update-support-design.md b/doc/plans/2026-02-09-remove-import-update-support-design.md new file mode 100644 index 0000000..889fb12 --- /dev/null +++ b/doc/plans/2026-02-09-remove-import-update-support-design.md @@ -0,0 +1,478 @@ +# 移除招聘信和采购交易导入更新支持功能设计文档 + +**日期:** 2026-02-09 +**模块:** 招聘信息管理、采购交易管理 +**类型:** 功能简化 + +## 1. 需求概述 + +### 1.1 背景 +当前招聘信息和采购交易信息模块的导入功能支持"导入更新"模式,允许用户通过导入文件来更新已存在的数据。但实际业务场景中,这两个模块不应该支持导入更新操作。 + +### 1.2 目标 +- 完全移除招聘信和采购交易的导入更新功能 +- 简化代码逻辑,降低维护成本 +- 导入时遇到已存在的数据直接报错,避免意外覆盖 + +### 1.3 处理策略 +- **遇到已存在数据:** 跳过该条数据,记录到失败列表 +- **错误提示:** 显示具体重复的数据ID(招聘项目编号/采购事项ID) +- **其他数据:** 继续正常导入 + +## 2. 技术方案 + +### 2.1 后端修改 + +#### 2.1.1 招聘信模块 + +**Controller层:** `CcdiStaffRecruitmentController.java` + +```java +// 修改前 +@PostMapping("/import") +public AjaxResult importRecruitment(@RequestParam("file") MultipartFile file, + @RequestParam Boolean isUpdateSupport) throws Exception { + // ... + importService.importRecruitmentAsync(excelList, isUpdateSupport, taskId, username); + // ... +} + +// 修改后 +@PostMapping("/import") +public AjaxResult importRecruitment(@RequestParam("file") MultipartFile file) throws Exception { + // ... + importService.importRecruitmentAsync(excelList, taskId, username); + // ... +} +``` + +**Service接口:** `ICcdiStaffRecruitmentImportService.java` + +```java +// 修改前 +void importRecruitmentAsync(List excelList, + Boolean isUpdateSupport, + String taskId, + String userName); + +// 修改后 +void importRecruitmentAsync(List excelList, + String taskId, + String userName); +``` + +**Service实现:** `CcdiStaffRecruitmentImportServiceImpl.java` + +主要修改点: +1. 移除方法参数 `Boolean isUpdateSupport` +2. 移除 `List updateRecords` 变量 +3. 简化数据分类逻辑(第73-92行) +4. 移除批量更新逻辑(第107-110行) +5. 修改错误提示信息 + +```java +// 修改后的数据分类逻辑 +for (CcdiStaffRecruitmentExcel excel : excelList) { + try { + // 验证数据(不再需要isUpdateSupport参数) + validateRecruitmentData(excel, existingRecruitIds); + + CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); + BeanUtils.copyProperties(excel, recruitment); + + if (existingRecruitIds.contains(excel.getRecruitId())) { + // 直接抛出异常,记录为失败 + throw new RuntimeException( + String.format("招聘项目编号[%s]已存在,请勿重复导入", excel.getRecruitId()) + ); + } else { + recruitment.setCreatedBy(userName); + recruitment.setUpdatedBy(userName); + newRecords.add(recruitment); + } + } catch (Exception e) { + RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(e.getMessage()); + failures.add(failure); + } +} + +// 移除批量更新代码 +// 删除以下代码: +// if (!updateRecords.isEmpty() && isUpdateSupport) { +// recruitmentMapper.updateBatch(updateRecords); +// } +``` + +**验证方法简化:** + +```java +// 修改前 +private void validateRecruitmentData(CcdiStaffRecruitmentExcel excel, + Boolean isUpdateSupport, + Set existingRecruitIds) + +// 修改后 +private void validateRecruitmentData(CcdiStaffRecruitmentExcel excel, + Set existingRecruitIds) +``` + +#### 2.1.2 采购交易模块 + +**Controller层:** `CcdiPurchaseTransactionController.java` + +```java +// 修改前 +@PostMapping("/import") +public AjaxResult importTransaction(@RequestParam("file") MultipartFile file, + @RequestParam Boolean isUpdateSupport) throws Exception { + // ... + importService.importTransactionAsync(excelList, isUpdateSupport, taskId, username); + // ... +} + +// 修改后 +@PostMapping("/import") +public AjaxResult importTransaction(@RequestParam("file") MultipartFile file) throws Exception { + // ... + importService.importTransactionAsync(excelList, taskId, username); + // ... +} +``` + +**Service接口:** `ICcdiPurchaseTransactionImportService.java` + +```java +// 修改前 +void importTransactionAsync(List excelList, + Boolean isUpdateSupport, + String taskId, + String userName); + +// 修改后 +void importTransactionAsync(List excelList, + String taskId, + String userName); +``` + +**Service实现:** `CcdiPurchaseTransactionImportServiceImpl.java` + +主要修改点: +1. 移除方法参数 `Boolean isUpdateSupport` +2. 移除 `List updateRecords` 变量 +3. 简化数据分类逻辑(第54-88行) +4. 移除批量更新逻辑(第95-98行) +5. 修改错误提示信息 + +```java +// 修改后的数据分类逻辑 +for (int i = 0; i < excelList.size(); i++) { + CcdiPurchaseTransactionExcel excel = excelList.get(i); + + try { + // 转换为AddDTO进行验证 + CcdiPurchaseTransactionAddDTO addDTO = new CcdiPurchaseTransactionAddDTO(); + BeanUtils.copyProperties(excel, addDTO); + + // 验证数据(不再需要isUpdateSupport参数) + validateTransactionData(addDTO, existingIds); + + CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction(); + BeanUtils.copyProperties(excel, transaction); + + if (existingIds.contains(excel.getPurchaseId())) { + // 直接抛出异常,记录为失败 + throw new RuntimeException( + String.format("采购事项ID[%s]已存在,请勿重复导入", excel.getPurchaseId()) + ); + } else { + transaction.setCreatedBy(userName); + transaction.setUpdatedBy(userName); + newRecords.add(transaction); + } + } catch (Exception e) { + PurchaseTransactionImportFailureVO failure = new PurchaseTransactionImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(e.getMessage()); + failures.add(failure); + } +} + +// 移除批量更新代码 +// 删除以下代码: +// if (!updateRecords.isEmpty() && isUpdateSupport) { +// transactionMapper.insertOrUpdateBatch(updateRecords); +// } +``` + +**验证方法简化:** + +```java +// 修改前 +private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO, + Boolean isUpdateSupport, + Set existingIds) + +// 修改后 +private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO, + Set existingIds) +``` + +### 2.2 前端修改 + +#### 2.2.1 招聘信模块 + +**文件:** `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue` + +**修改1:** 移除 `upload` 对象中的 `updateSupport` 字段 + +```javascript +// 修改前 (约第461行) +upload: { + // 是否显示弹出层 + open: false, + // 弹出层标题 + title: "", + // 是否禁用上传 + isUploading: false, + // 是否更新已经存在的招聘信息数据 + updateSupport: 0, + // 设置上传的请求头部 + headers: { Authorization: "Bearer " + getToken() }, + // 上传的地址 + url: process.env.VUE_APP_BASE_API + "/ccdi/staffRecruitment/import" +} + +// 修改后 +upload: { + // 是否显示弹出层 + open: false, + // 弹出层标题 + title: "", + // 是否禁用上传 + isUploading: false, + // 设置上传的请求头部 + headers: { Authorization: "Bearer " + getToken() }, + // 上传的地址 + url: process.env.VUE_APP_BASE_API + "/ccdi/staffRecruitment/import" +} +``` + +**修改2:** 移除导入对话框中的"是否更新"复选框 (约第327行) + +```html + +是否更新已经存在的招聘信息数据 +``` + +**修改3:** 移除URL中的updateSupport参数 (约第317行) + +```html + + + + + + + +``` + +#### 2.2.2 采购交易模块 + +**文件:** `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue` + +**修改1:** 移除 `upload` 对象中的 `updateSupport` 字段 + +```javascript +// 修改前 (约第719行) +upload: { + // 是否显示弹出层 + open: false, + // 弹出层标题 + title: "", + // 是否禁用上传 + isUploading: false, + // 是否更新已经存在的采购交易数据 + updateSupport: 0, + // 设置上传的请求头部 + headers: { Authorization: "Bearer " + getToken() }, + // 上传的地址 + url: process.env.VUE_APP_BASE_API + "/ccdi/purchaseTransaction/import" +} + +// 修改后 +upload: { + // 是否显示弹出层 + open: false, + // 弹出层标题 + title: "", + // 是否禁用上传 + isUploading: false, + // 设置上传的请求头部 + headers: { Authorization: "Bearer " + getToken() }, + // 上传的地址 + url: process.env.VUE_APP_BASE_API + "/ccdi/purchaseTransaction/import" +} +``` + +**修改2:** 移除导入对话框中的"是否更新"复选框 (约第513行) + +```html + +是否更新已经存在的采购交易数据 +``` + +**修改3:** 移除URL中的updateSupport参数 (约第503行) + +```html + + + + + + + +``` + +## 3. 数据流变化 + +### 3.1 修改前 + +``` +用户上传文件 + → 前端传递 isUpdateSupport 参数 + → 后端检查数据是否存在 + → 存在且 isUpdateSupport=true: 更新数据 + → 存在且 isUpdateSupport=false: 报错 + → 不存在: 新增数据 +``` + +### 3.2 修改后 + +``` +用户上传文件 + → 后端检查数据是否存在 + → 存在: 报错(显示重复ID),记录为失败 + → 不存在: 新增数据 +``` + +## 4. 代码变更统计 + +### 4.1 后端变更 + +| 模块 | 文件 | 变更类型 | 变更行数(预估) | +|------|------|----------|---------------| +| 招聘信 | CcdiStaffRecruitmentController.java | 修改 | ~5行 | +| 招聘信 | ICcdiStaffRecruitmentImportService.java | 修改 | ~3行 | +| 招聘信 | CcdiStaffRecruitmentImportServiceImpl.java | 修改/删除 | ~30行 | +| 采购交易 | CcdiPurchaseTransactionController.java | 修改 | ~5行 | +| 采购交易 | ICcdiPurchaseTransactionImportService.java | 修改 | ~3行 | +| 采购交易 | CcdiPurchaseTransactionImportServiceImpl.java | 修改/删除 | ~30行 | + +**总计:** 约76行 + +### 4.2 前端变更 + +| 模块 | 文件 | 变更类型 | 变更行数(预估) | +|------|------|----------|---------------| +| 招聘信 | index.vue | 修改/删除 | ~10行 | +| 采购交易 | index.vue | 修改/删除 | ~10行 | + +**总计:** 约20行 + +## 5. 测试计划 + +### 5.1 功能测试 + +**测试场景1: 导入全新数据** +- 输入: 导入文件中的所有数据都不存在于数据库 +- 预期: 全部导入成功,成功数=总数 + +**测试场景2: 导入部分重复数据** +- 输入: 导入文件中包含部分已存在的招聘项目编号/采购事项ID +- 预期: + - 已存在的数据记录为失败 + - 失败信息显示具体的重复ID + - 其他数据正常导入 + +**测试场景3: 导入全部重复数据** +- 输入: 导入文件中的所有数据都已存在 +- 预期: 全部导入失败,失败数=总数 + +**测试场景4: 前端UI验证** +- 检查导入对话框中不再显示"是否更新"复选框 +- 检查上传请求URL中不包含updateSupport参数 + +### 5.2 接口测试 + +使用测试脚本验证后端接口: +1. 不传递isUpdateSupport参数,接口应正常工作 +2. 验证重复数据的错误提示信息格式 + +## 6. 风险评估 + +### 6.1 兼容性风险 +- **风险:** 旧版前端可能会传递isUpdateSupport参数 +- **影响:** 后端接口会报参数错误 +- **缓解:** 确保前后端同时部署,或后端暂时兼容接收该参数但不处理 + +### 6.2 用户体验风险 +- **风险:** 用户习惯使用"导入更新"功能 +- **影响:** 需要先删除旧数据再导入新数据 +- **缓解:** 在失败提示中明确告知数据ID,方便用户删除 + +### 6.3 数据一致性风险 +- **风险:** 低风险,因为只是移除更新功能 +- **影响:** 无 +- **缓解:** 无需特殊处理 + +## 7. 部署建议 + +### 7.1 部署顺序 +1. 先部署后端代码 +2. 再部署前端代码 +3. 前后端必须同时上线,避免调用失败 + +### 7.2 数据库变更 +- 无需数据库变更 + +### 7.3 配置变更 +- 无需配置变更 + +## 8. 回滚方案 + +如果需要回滚,可以: +1. 恢复后端代码,恢复isUpdateSupport参数处理逻辑 +2. 恢复前端代码,恢复"是否更新"复选框 + +## 9. 附录 + +### 9.1 相关文档 +- 招聘信息导入功能设计: `doc/plans/2026-02-06-recruitment-async-import-design.md` +- 采购交易导入功能设计: `doc/plans/2026-02-08-purchase-transaction-import-design.md` + +### 9.2 相关API文档 +- 招聘信息API: `doc/api/ccdi_staff_recruitment_api.md` +- 采购交易API: `doc/api/ccdi_purchase_transaction_api.md` + +--- + +**审批记录** + +| 角色 | 姓名 | 日期 | 状态 | +|------|------|------|------| +| 开发 | - | 2026-02-09 | 待审批 | +| 审批 | - | - | 待审批 | diff --git a/doc/test-data/employee/employee_test_data_1000 - 副本 (3).xlsx b/doc/test-data/employee/employee_test_data_1000 - 副本 (3).xlsx new file mode 100644 index 0000000..6460647 Binary files /dev/null and b/doc/test-data/employee/employee_test_data_1000 - 副本 (3).xlsx differ diff --git a/doc/test-data/employee/employee_test_data_phone.xlsx b/doc/test-data/employee/employee_test_data_phone.xlsx new file mode 100644 index 0000000..41c10b8 Binary files /dev/null and b/doc/test-data/employee/employee_test_data_phone.xlsx differ diff --git a/doc/test-data/recruitment/recruitment_test_data_100.xlsx b/doc/test-data/recruitment/recruitment_test_data_100.xlsx new file mode 100644 index 0000000..5aeceed Binary files /dev/null and b/doc/test-data/recruitment/recruitment_test_data_100.xlsx differ diff --git a/doc/test-data/recruitment/recruitment_test_data_1000_part1.xlsx b/doc/test-data/recruitment/recruitment_test_data_1000_part1.xlsx new file mode 100644 index 0000000..4219973 Binary files /dev/null and b/doc/test-data/recruitment/recruitment_test_data_1000_part1.xlsx differ diff --git a/doc/test-data/recruitment/recruitment_test_data_1000_part2.xlsx b/doc/test-data/recruitment/recruitment_test_data_1000_part2.xlsx new file mode 100644 index 0000000..dc4b99d Binary files /dev/null and b/doc/test-data/recruitment/recruitment_test_data_1000_part2.xlsx differ diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java index 86f62ba..d5122bf 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java @@ -1,23 +1,13 @@ package com.ruoyi.ccdi.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.ruoyi.ccdi.domain.dto.CcdiIntermediaryEntityAddDTO; -import com.ruoyi.ccdi.domain.dto.CcdiIntermediaryEntityEditDTO; -import com.ruoyi.ccdi.domain.dto.CcdiIntermediaryPersonAddDTO; -import com.ruoyi.ccdi.domain.dto.CcdiIntermediaryPersonEditDTO; -import com.ruoyi.ccdi.domain.dto.CcdiIntermediaryQueryDTO; +import com.ruoyi.ccdi.domain.dto.*; import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryEntityExcel; import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryPersonExcel; -import com.ruoyi.ccdi.domain.vo.CcdiIntermediaryEntityDetailVO; -import com.ruoyi.ccdi.domain.vo.CcdiIntermediaryPersonDetailVO; -import com.ruoyi.ccdi.domain.vo.CcdiIntermediaryVO; -import com.ruoyi.ccdi.domain.vo.ImportResultVO; -import com.ruoyi.ccdi.domain.vo.ImportStatusVO; -import com.ruoyi.ccdi.domain.vo.IntermediaryPersonImportFailureVO; -import com.ruoyi.ccdi.domain.vo.IntermediaryEntityImportFailureVO; -import com.ruoyi.ccdi.service.ICcdiIntermediaryService; -import com.ruoyi.ccdi.service.ICcdiIntermediaryPersonImportService; +import com.ruoyi.ccdi.domain.vo.*; import com.ruoyi.ccdi.service.ICcdiIntermediaryEntityImportService; +import com.ruoyi.ccdi.service.ICcdiIntermediaryPersonImportService; +import com.ruoyi.ccdi.service.ICcdiIntermediaryService; import com.ruoyi.ccdi.utils.EasyExcelUtil; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; @@ -26,7 +16,6 @@ import com.ruoyi.common.core.page.PageDomain; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableSupport; import com.ruoyi.common.enums.BusinessType; -import com.ruoyi.common.utils.StringUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -193,7 +182,7 @@ public class CcdiIntermediaryController extends BaseController { @PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')") @Log(title = "个人中介", businessType = BusinessType.IMPORT) @PostMapping("/importPersonData") - public AjaxResult importPersonData(MultipartFile file, boolean updateSupport) throws Exception { + public AjaxResult importPersonData(MultipartFile file) throws Exception { List list = EasyExcelUtil.importExcel( file.getInputStream(), CcdiIntermediaryPersonExcel.class); @@ -202,7 +191,7 @@ public class CcdiIntermediaryController extends BaseController { } // 提交异步任务 - String taskId = intermediaryService.importIntermediaryPerson(list, updateSupport); + String taskId = intermediaryService.importIntermediaryPerson(list); // 立即返回,不等待后台任务完成 ImportResultVO result = new ImportResultVO(); @@ -220,7 +209,7 @@ public class CcdiIntermediaryController extends BaseController { @PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')") @Log(title = "实体中介", businessType = BusinessType.IMPORT) @PostMapping("/importEntityData") - public AjaxResult importEntityData(MultipartFile file, boolean updateSupport) throws Exception { + public AjaxResult importEntityData(MultipartFile file) throws Exception { List list = EasyExcelUtil.importExcel( file.getInputStream(), CcdiIntermediaryEntityExcel.class); @@ -229,7 +218,7 @@ public class CcdiIntermediaryController extends BaseController { } // 提交异步任务 - String taskId = intermediaryService.importIntermediaryEntity(list, updateSupport); + String taskId = intermediaryService.importIntermediaryEntity(list); // 立即返回,不等待后台任务完成 ImportResultVO result = new ImportResultVO(); diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiStaffRecruitmentController.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiStaffRecruitmentController.java index 25c6f52..34584de 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiStaffRecruitmentController.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiStaffRecruitmentController.java @@ -6,6 +6,10 @@ import com.ruoyi.ccdi.domain.dto.CcdiStaffRecruitmentEditDTO; import com.ruoyi.ccdi.domain.dto.CcdiStaffRecruitmentQueryDTO; import com.ruoyi.ccdi.domain.excel.CcdiStaffRecruitmentExcel; import com.ruoyi.ccdi.domain.vo.CcdiStaffRecruitmentVO; +import com.ruoyi.ccdi.domain.vo.ImportResultVO; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.domain.vo.RecruitmentImportFailureVO; +import com.ruoyi.ccdi.service.ICcdiStaffRecruitmentImportService; import com.ruoyi.ccdi.service.ICcdiStaffRecruitmentService; import com.ruoyi.ccdi.utils.EasyExcelUtil; import com.ruoyi.common.annotation.Log; @@ -16,6 +20,7 @@ import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableSupport; import com.ruoyi.common.enums.BusinessType; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; @@ -40,6 +45,9 @@ public class CcdiStaffRecruitmentController extends BaseController { @Resource private ICcdiStaffRecruitmentService recruitmentService; + @Resource + private ICcdiStaffRecruitmentImportService recruitmentImportService; + /** * 查询招聘信息列表 */ @@ -120,15 +128,66 @@ public class CcdiStaffRecruitmentController extends BaseController { } /** - * 导入招聘信息 + * 异步导入招聘信息 */ - @Operation(summary = "导入招聘信息") + @Operation(summary = "异步导入招聘信息") + @Parameter(name = "file", description = "导入文件", required = true) @PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:import')") @Log(title = "员工招聘信息", businessType = BusinessType.IMPORT) @PostMapping("/importData") - public AjaxResult importData(MultipartFile file) throws Exception { + public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception { List list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffRecruitmentExcel.class); - String message = recruitmentService.importRecruitment(list); - return success(message); + + if (list == null || list.isEmpty()) { + return error("至少需要一条数据"); + } + + // 提交异步任务 + String taskId = recruitmentService.importRecruitment(list); + + // 立即返回,不等待后台任务完成 + ImportResultVO result = new ImportResultVO(); + result.setTaskId(taskId); + result.setStatus("PROCESSING"); + result.setMessage("导入任务已提交,正在后台处理"); + + return AjaxResult.success("导入任务已提交,正在后台处理", result); + } + + /** + * 查询导入状态 + */ + @Operation(summary = "查询导入状态") + @Parameter(name = "taskId", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:import')") + @GetMapping("/importStatus/{taskId}") + public AjaxResult getImportStatus(@PathVariable String taskId) { + ImportStatusVO statusVO = recruitmentImportService.getImportStatus(taskId); + return success(statusVO); + } + + /** + * 查询导入失败记录 + */ + @Operation(summary = "查询导入失败记录") + @Parameter(name = "taskId", description = "任务ID", required = true) + @Parameter(name = "pageNum", description = "页码", required = false) + @Parameter(name = "pageSize", description = "每页条数", required = false) + @PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:import')") + @GetMapping("/importFailures/{taskId}") + public TableDataInfo getImportFailures( + @PathVariable String taskId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + + List failures = recruitmentImportService.getImportFailures(taskId); + + // 手动分页 + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, failures.size()); + + List pageData = failures.subList(fromIndex, toIndex); + + return getDataTable(pageData, failures.size()); } } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java index 74f5665..99996e3 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java @@ -17,13 +17,11 @@ public interface ICcdiIntermediaryEntityImportService { /** * 异步导入实体中介数据 * - * @param excelList Excel数据列表 - * @param isUpdateSupport 是否更新已存在的数据 - * @param taskId 任务ID - * @param userName 当前用户名(用于审计字段) + * @param excelList Excel数据列表 + * @param taskId 任务ID + * @param userName 当前用户名(用于审计字段) */ void importEntityAsync(List excelList, - Boolean isUpdateSupport, String taskId, String userName); diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java index 37fab76..a844c3c 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java @@ -17,13 +17,11 @@ public interface ICcdiIntermediaryPersonImportService { /** * 异步导入个人中介数据 * - * @param excelList Excel数据列表 - * @param isUpdateSupport 是否更新已存在的数据 - * @param taskId 任务ID - * @param userName 当前用户名(用于审计字段) + * @param excelList Excel数据列表 + * @param taskId 任务ID + * @param userName 当前用户名(用于审计字段) */ void importPersonAsync(List excelList, - Boolean isUpdateSupport, String taskId, String userName); diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryService.java index a359c0b..6cd2cab 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryService.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryService.java @@ -101,17 +101,15 @@ public interface ICcdiIntermediaryService { * 导入个人中介数据 * * @param list Excel实体列表 - * @param updateSupport 是否更新支持 * @return 结果 */ - String importIntermediaryPerson(java.util.List list, boolean updateSupport); + String importIntermediaryPerson(java.util.List list); /** * 导入实体中介数据 * * @param list Excel实体列表 - * @param updateSupport 是否更新支持 * @return 结果 */ - String importIntermediaryEntity(java.util.List list, boolean updateSupport); + String importIntermediaryEntity(java.util.List list); } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java index ecb19a4..fa99c8a 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java @@ -17,12 +17,11 @@ public interface ICcdiStaffRecruitmentImportService { /** * 异步导入招聘信息数据 * - * @param excelList Excel数据列表 - * @param isUpdateSupport 是否更新已存在的数据 - * @param taskId 任务ID + * @param excelList Excel数据列表 + * @param taskId 任务ID + * @param userName 用户名 */ void importRecruitmentAsync(List excelList, - Boolean isUpdateSupport, String taskId, String userName); diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java index aed0c75..28900b7 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java @@ -42,20 +42,24 @@ public class CcdiIntermediaryEntityImportServiceImpl implements ICcdiIntermediar @Async @Transactional(rollbackFor = Exception.class) public void importEntityAsync(List excelList, - Boolean isUpdateSupport, String taskId, String userName) { - List validRecords = new ArrayList<>(); + List newRecords = new ArrayList<>(); List failures = new ArrayList<>(); - // 1. 批量查询已存在的统一社会信用代码 + // 批量查询已存在的统一社会信用代码 Set existingCreditCodes = getExistingCreditCodes(excelList); - // 2. 验证并转换数据 - for (CcdiIntermediaryEntityExcel excel : excelList) { + // 用于检测Excel内部的重复ID + Set excelProcessedIds = new HashSet<>(); + + // 分类数据 + for (int i = 0; i < excelList.size(); i++) { + CcdiIntermediaryEntityExcel excel = excelList.get(i); + try { // 验证数据 - validateEntityData(excel, isUpdateSupport, existingCreditCodes); + validateEntityData(excel, existingCreditCodes); CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo(); BeanUtils.copyProperties(excel, entity); @@ -64,57 +68,41 @@ public class CcdiIntermediaryEntityImportServiceImpl implements ICcdiIntermediar entity.setDataSource("IMPORT"); entity.setEntSource("INTERMEDIARY"); entity.setCreatedBy(userName); - if (existingCreditCodes.contains(excel.getSocialCreditCode()) && isUpdateSupport) { - entity.setUpdatedBy(userName); - } + entity.setUpdatedBy(userName); - validRecords.add(entity); + if (existingCreditCodes.contains(excel.getSocialCreditCode())) { + // 统一社会信用代码在数据库中已存在,直接报错 + throw new RuntimeException(String.format("统一社会信用代码[%s]已存在,请勿重复导入", excel.getSocialCreditCode())); + } else if (excelProcessedIds.contains(excel.getSocialCreditCode())) { + // 统一社会信用代码在Excel文件内部重复 + throw new RuntimeException(String.format("统一社会信用代码[%s]在导入文件中重复,已跳过此条记录", excel.getSocialCreditCode())); + } else { + newRecords.add(entity); + excelProcessedIds.add(excel.getSocialCreditCode()); // 标记为已处理 + } } catch (Exception e) { failures.add(createFailureVO(excel, e.getMessage())); } } - // 3. 根据isUpdateSupport选择处理方式 - int actualSuccessCount = 0; - if (isUpdateSupport) { - // 更新模式:直接批量导入,数据库自动处理INSERT或UPDATE - if (!validRecords.isEmpty()) { - actualSuccessCount = saveBatchWithUpsert(validRecords, 500); - } - } else { - // 仅新增模式:先查询已存在的记录,对冲突的抛出异常 - Set actualExistingCreditCodes = getExistingCreditCodesFromDb(validRecords); - List actualNewRecords = new ArrayList<>(); - - for (CcdiEnterpriseBaseInfo record : validRecords) { - if (actualExistingCreditCodes.contains(record.getSocialCreditCode())) { - // 记录到失败列表 - failures.add(createFailureVO(record, "该统一社会信用代码已存在")); - } else { - actualNewRecords.add(record); - } - } - - // 批量插入新记录 - if (!actualNewRecords.isEmpty()) { - int insertCount = saveBatch(actualNewRecords, 500); - actualSuccessCount = insertCount; - } + // 批量插入新数据 + if (!newRecords.isEmpty()) { + saveBatch(newRecords, 500); } - // 4. 保存失败记录到Redis + // 保存失败记录到Redis if (!failures.isEmpty()) { String failuresKey = "import:intermediary-entity:" + taskId + ":failures"; redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); } - // 5. 更新最终状态 ImportResult result = new ImportResult(); result.setTotalCount(excelList.size()); - result.setSuccessCount(actualSuccessCount); + result.setSuccessCount(newRecords.size()); result.setFailureCount(failures.size()); + // 更新最终状态 String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; updateImportStatus(taskId, finalStatus, result); } @@ -273,11 +261,9 @@ public class CcdiIntermediaryEntityImportServiceImpl implements ICcdiIntermediar * 验证实体中介数据 * * @param excel Excel数据 - * @param isUpdateSupport 是否支持更新 * @param existingCreditCodes 已存在的统一社会信用代码集合 */ private void validateEntityData(CcdiIntermediaryEntityExcel excel, - Boolean isUpdateSupport, Set existingCreditCodes) { // 验证必填字段:机构名称 if (StringUtils.isEmpty(excel.getEnterpriseName())) { @@ -289,18 +275,9 @@ public class CcdiIntermediaryEntityImportServiceImpl implements ICcdiIntermediar throw new RuntimeException("统一社会信用代码不能为空"); } - // 如果统一社会信用代码已存在但未启用更新支持,抛出异常 - if (existingCreditCodes.contains(excel.getSocialCreditCode()) && !isUpdateSupport) { - throw new RuntimeException("该统一社会信用代码已存在"); - } - - // 如果统一社会信用代码不存在,检查唯一性 - if (!existingCreditCodes.contains(excel.getSocialCreditCode())) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CcdiEnterpriseBaseInfo::getSocialCreditCode, excel.getSocialCreditCode()); - if (entityMapper.selectCount(wrapper) > 0) { - throw new RuntimeException("该统一社会信用代码已存在"); - } + // 统一社会信用代码格式验证(18位) + if (excel.getSocialCreditCode().length() != 18) { + throw new RuntimeException("统一社会信用代码必须为18位"); } } } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java index 5db24c8..7f3b1e2 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java @@ -43,20 +43,24 @@ public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediar @Async @Transactional(rollbackFor = Exception.class) public void importPersonAsync(List excelList, - Boolean isUpdateSupport, String taskId, String userName) { - List validRecords = new ArrayList<>(); + List newRecords = new ArrayList<>(); List failures = new ArrayList<>(); - // 1. 批量查询已存在的证件号 + // 批量查询已存在的证件号 Set existingPersonIds = getExistingPersonIds(excelList); - // 2. 验证并转换数据 - for (CcdiIntermediaryPersonExcel excel : excelList) { + // 用于检测Excel内部的重复ID + Set excelProcessedIds = new HashSet<>(); + + // 分类数据 + for (int i = 0; i < excelList.size(); i++) { + CcdiIntermediaryPersonExcel excel = excelList.get(i); + try { // 验证数据 - validatePersonData(excel, isUpdateSupport, existingPersonIds); + validatePersonData(excel, existingPersonIds); CcdiBizIntermediary intermediary = new CcdiBizIntermediary(); BeanUtils.copyProperties(excel, intermediary); @@ -64,57 +68,41 @@ public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediar // 设置数据来源和审计字段 intermediary.setDataSource("IMPORT"); intermediary.setCreatedBy(userName); - if (existingPersonIds.contains(excel.getPersonId()) && isUpdateSupport) { - intermediary.setUpdatedBy(userName); - } + intermediary.setUpdatedBy(userName); - validRecords.add(intermediary); + if (existingPersonIds.contains(excel.getPersonId())) { + // 证件号码在数据库中已存在,直接报错 + throw new RuntimeException(String.format("证件号码[%s]已存在,请勿重复导入", excel.getPersonId())); + } else if (excelProcessedIds.contains(excel.getPersonId())) { + // 证件号码在Excel文件内部重复 + throw new RuntimeException(String.format("证件号码[%s]在导入文件中重复,已跳过此条记录", excel.getPersonId())); + } else { + newRecords.add(intermediary); + excelProcessedIds.add(excel.getPersonId()); // 标记为已处理 + } } catch (Exception e) { failures.add(createFailureVO(excel, e.getMessage())); } } - // 3. 根据isUpdateSupport选择处理方式 - int actualSuccessCount = 0; - if (isUpdateSupport) { - // 更新模式:直接批量导入,数据库自动处理INSERT或UPDATE - if (!validRecords.isEmpty()) { - actualSuccessCount = saveBatchWithUpsert(validRecords, 500); - } - } else { - // 仅新增模式:先查询已存在的记录,对冲突的抛出异常 - Set actualExistingPersonIds = getExistingPersonIdsFromDb(validRecords); - List actualNewRecords = new ArrayList<>(); - - for (CcdiBizIntermediary record : validRecords) { - if (actualExistingPersonIds.contains(record.getPersonId())) { - // 记录到失败列表 - failures.add(createFailureVO(record, "该证件号码已存在")); - } else { - actualNewRecords.add(record); - } - } - - // 批量插入新记录 - if (!actualNewRecords.isEmpty()) { - int insertCount = saveBatch(actualNewRecords, 500); - actualSuccessCount = insertCount; - } + // 批量插入新数据 + if (!newRecords.isEmpty()) { + saveBatch(newRecords, 500); } - // 4. 保存失败记录到Redis + // 保存失败记录到Redis if (!failures.isEmpty()) { String failuresKey = "import:intermediary:" + taskId + ":failures"; redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); } - // 5. 更新最终状态 ImportResult result = new ImportResult(); result.setTotalCount(excelList.size()); - result.setSuccessCount(actualSuccessCount); + result.setSuccessCount(newRecords.size()); result.setFailureCount(failures.size()); + // 更新最终状态 String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; updateImportStatus(taskId, finalStatus, result); } @@ -273,11 +261,9 @@ public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediar * 验证个人中介数据 * * @param excel Excel数据 - * @param isUpdateSupport 是否支持更新 * @param existingPersonIds 已存在的证件号集合 */ private void validatePersonData(CcdiIntermediaryPersonExcel excel, - Boolean isUpdateSupport, Set existingPersonIds) { // 验证必填字段:姓名 if (StringUtils.isEmpty(excel.getName())) { @@ -294,19 +280,5 @@ public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediar if (idCardError != null) { throw new RuntimeException("证件号码" + idCardError); } - - // 如果证件号已存在但未启用更新支持,抛出异常 - if (existingPersonIds.contains(excel.getPersonId()) && !isUpdateSupport) { - throw new RuntimeException("该证件号码已存在"); - } - - // 如果证件号不存在,检查唯一性 - if (!existingPersonIds.contains(excel.getPersonId())) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CcdiBizIntermediary::getPersonId, excel.getPersonId()); - if (intermediaryMapper.selectCount(wrapper) > 0) { - throw new RuntimeException("该证件号码已存在"); - } - } } } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java index b41068a..e2b4d38 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java @@ -13,9 +13,9 @@ import com.ruoyi.ccdi.domain.vo.CcdiIntermediaryVO; import com.ruoyi.ccdi.mapper.CcdiBizIntermediaryMapper; import com.ruoyi.ccdi.mapper.CcdiEnterpriseBaseInfoMapper; import com.ruoyi.ccdi.mapper.CcdiIntermediaryMapper; -import com.ruoyi.ccdi.service.ICcdiIntermediaryService; -import com.ruoyi.ccdi.service.ICcdiIntermediaryPersonImportService; import com.ruoyi.ccdi.service.ICcdiIntermediaryEntityImportService; +import com.ruoyi.ccdi.service.ICcdiIntermediaryPersonImportService; +import com.ruoyi.ccdi.service.ICcdiIntermediaryService; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import jakarta.annotation.Resource; @@ -24,7 +24,6 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -258,14 +257,12 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { /** * 导入个人中介数据(异步) * - * @param list Excel实体列表 - * @param updateSupport 是否更新支持 + * @param list Excel实体列表 * @return 任务ID */ @Override @Transactional - public String importIntermediaryPerson(List list, - boolean updateSupport) { + public String importIntermediaryPerson(List list) { String taskId = UUID.randomUUID().toString(); long startTime = System.currentTimeMillis(); @@ -288,7 +285,7 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { String userName = SecurityUtils.getUsername(); // 调用异步方法 - personImportService.importPersonAsync(list, updateSupport, taskId, userName); + personImportService.importPersonAsync(list, taskId, userName); return taskId; } @@ -296,14 +293,12 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { /** * 导入实体中介数据(异步) * - * @param list Excel实体列表 - * @param updateSupport 是否更新支持 + * @param list Excel实体列表 * @return 任务ID */ @Override @Transactional - public String importIntermediaryEntity(List list, - boolean updateSupport) { + public String importIntermediaryEntity(List list) { String taskId = UUID.randomUUID().toString(); long startTime = System.currentTimeMillis(); @@ -326,7 +321,7 @@ public class CcdiIntermediaryServiceImpl implements ICcdiIntermediaryService { String userName = SecurityUtils.getUsername(); // 调用异步方法 - entityImportService.importEntityAsync(list, updateSupport, taskId, userName); + entityImportService.importEntityAsync(list, taskId, userName); return taskId; } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java index 734efb8..1911e4d 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java @@ -3,6 +3,7 @@ package com.ruoyi.ccdi.service.impl; import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.ccdi.domain.CcdiStaffRecruitment; +import com.ruoyi.ccdi.domain.dto.CcdiStaffRecruitmentAddDTO; import com.ruoyi.ccdi.domain.excel.CcdiStaffRecruitmentExcel; import com.ruoyi.ccdi.domain.vo.ImportResult; import com.ruoyi.ccdi.domain.vo.ImportStatusVO; @@ -18,6 +19,7 @@ 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 org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.concurrent.TimeUnit; @@ -41,54 +43,45 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm @Override @Async + @Transactional public void importRecruitmentAsync(List excelList, - Boolean isUpdateSupport, String taskId, String userName) { - long startTime = System.currentTimeMillis(); - - // 初始化Redis状态 - String statusKey = "import:recruitment:" + taskId; - Map statusData = new HashMap<>(); - statusData.put("taskId", taskId); - statusData.put("status", "PROCESSING"); - statusData.put("totalCount", excelList.size()); - statusData.put("successCount", 0); - statusData.put("failureCount", 0); - statusData.put("progress", 0); - statusData.put("startTime", startTime); - statusData.put("message", "正在处理..."); - - redisTemplate.opsForHash().putAll(statusKey, statusData); - redisTemplate.expire(statusKey, 7, TimeUnit.DAYS); - List newRecords = new ArrayList<>(); - List updateRecords = new ArrayList<>(); List failures = new ArrayList<>(); // 批量查询已存在的招聘项目编号 Set existingRecruitIds = getExistingRecruitIds(excelList); + // 用于检测Excel内部的重复ID + Set excelProcessedIds = new HashSet<>(); + // 分类数据 - for (CcdiStaffRecruitmentExcel excel : excelList) { + for (int i = 0; i < excelList.size(); i++) { + CcdiStaffRecruitmentExcel excel = excelList.get(i); + try { + // 转换为AddDTO进行验证 + CcdiStaffRecruitmentAddDTO addDTO = new CcdiStaffRecruitmentAddDTO(); + BeanUtils.copyProperties(excel, addDTO); + // 验证数据 - validateRecruitmentData(excel, isUpdateSupport, existingRecruitIds); + validateRecruitmentData(addDTO, existingRecruitIds); CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); BeanUtils.copyProperties(excel, recruitment); if (existingRecruitIds.contains(excel.getRecruitId())) { - if (isUpdateSupport) { - recruitment.setUpdatedBy(userName); - updateRecords.add(recruitment); - } else { - throw new RuntimeException("该招聘项目编号已存在"); - } + // 招聘项目编号在数据库中已存在,直接报错 + throw new RuntimeException(String.format("招聘项目编号[%s]已存在,请勿重复导入", excel.getRecruitId())); + } else if (excelProcessedIds.contains(excel.getRecruitId())) { + // 招聘项目编号在Excel文件内部重复 + throw new RuntimeException(String.format("招聘项目编号[%s]在导入文件中重复,已跳过此条记录", excel.getRecruitId())); } else { recruitment.setCreatedBy(userName); recruitment.setUpdatedBy(userName); newRecords.add(recruitment); + excelProcessedIds.add(excel.getRecruitId()); // 标记为已处理 } } catch (Exception e) { @@ -101,12 +94,7 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm // 批量插入新数据 if (!newRecords.isEmpty()) { - recruitmentMapper.insertBatch(newRecords); - } - - // 批量更新已有数据 - if (!updateRecords.isEmpty() && isUpdateSupport) { - recruitmentMapper.updateBatch(updateRecords); + saveBatch(newRecords, 500); } // 保存失败记录到Redis @@ -115,12 +103,12 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); } - // 更新最终状态 ImportResult result = new ImportResult(); result.setTotalCount(excelList.size()); - result.setSuccessCount(newRecords.size() + updateRecords.size()); + result.setSuccessCount(newRecords.size()); result.setFailureCount(failures.size()); + // 更新最终状态 String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; updateImportStatus(taskId, finalStatus, result); } @@ -187,60 +175,59 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm /** * 验证招聘信息数据 */ - private void validateRecruitmentData(CcdiStaffRecruitmentExcel excel, - Boolean isUpdateSupport, + private void validateRecruitmentData(CcdiStaffRecruitmentAddDTO addDTO, Set existingRecruitIds) { // 验证必填字段 - if (StringUtils.isEmpty(excel.getRecruitId())) { + if (StringUtils.isEmpty(addDTO.getRecruitId())) { throw new RuntimeException("招聘项目编号不能为空"); } - if (StringUtils.isEmpty(excel.getRecruitName())) { + if (StringUtils.isEmpty(addDTO.getRecruitName())) { throw new RuntimeException("招聘项目名称不能为空"); } - if (StringUtils.isEmpty(excel.getPosName())) { + if (StringUtils.isEmpty(addDTO.getPosName())) { throw new RuntimeException("职位名称不能为空"); } - if (StringUtils.isEmpty(excel.getPosCategory())) { + if (StringUtils.isEmpty(addDTO.getPosCategory())) { throw new RuntimeException("职位类别不能为空"); } - if (StringUtils.isEmpty(excel.getPosDesc())) { + if (StringUtils.isEmpty(addDTO.getPosDesc())) { throw new RuntimeException("职位描述不能为空"); } - if (StringUtils.isEmpty(excel.getCandName())) { + if (StringUtils.isEmpty(addDTO.getCandName())) { throw new RuntimeException("应聘人员姓名不能为空"); } - if (StringUtils.isEmpty(excel.getCandEdu())) { + if (StringUtils.isEmpty(addDTO.getCandEdu())) { throw new RuntimeException("应聘人员学历不能为空"); } - if (StringUtils.isEmpty(excel.getCandId())) { + if (StringUtils.isEmpty(addDTO.getCandId())) { throw new RuntimeException("证件号码不能为空"); } - if (StringUtils.isEmpty(excel.getCandSchool())) { + if (StringUtils.isEmpty(addDTO.getCandSchool())) { throw new RuntimeException("应聘人员毕业院校不能为空"); } - if (StringUtils.isEmpty(excel.getCandMajor())) { + if (StringUtils.isEmpty(addDTO.getCandMajor())) { throw new RuntimeException("应聘人员专业不能为空"); } - if (StringUtils.isEmpty(excel.getCandGrad())) { + if (StringUtils.isEmpty(addDTO.getCandGrad())) { throw new RuntimeException("应聘人员毕业年月不能为空"); } - if (StringUtils.isEmpty(excel.getAdmitStatus())) { + if (StringUtils.isEmpty(addDTO.getAdmitStatus())) { throw new RuntimeException("录用情况不能为空"); } // 验证证件号码格式 - String idCardError = IdCardUtil.getErrorMessage(excel.getCandId()); + String idCardError = IdCardUtil.getErrorMessage(addDTO.getCandId()); if (idCardError != null) { throw new RuntimeException("证件号码" + idCardError); } // 验证毕业年月格式(YYYYMM) - if (!excel.getCandGrad().matches("^((19|20)\\d{2})(0[1-9]|1[0-2])$")) { + if (!addDTO.getCandGrad().matches("^((19|20)\\d{2})(0[1-9]|1[0-2])$")) { throw new RuntimeException("毕业年月格式不正确,应为YYYYMM"); } // 验证录用状态 - if (AdmitStatus.getDescByCode(excel.getAdmitStatus()) == null) { + if (AdmitStatus.getDescByCode(addDTO.getAdmitStatus()) == null) { throw new RuntimeException("录用情况只能填写'录用'、'未录用'或'放弃'"); } } @@ -266,4 +253,36 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm redisTemplate.opsForHash().putAll(key, statusData); } + + /** + * 批量保存 + */ + 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); + + // 过滤掉已存在的记录,防止主键冲突 + List recruitIds = subList.stream() + .map(CcdiStaffRecruitment::getRecruitId) + .collect(Collectors.toList()); + + if (!recruitIds.isEmpty()) { + List existingRecords = recruitmentMapper.selectBatchIds(recruitIds); + Set existingIds = existingRecords.stream() + .map(CcdiStaffRecruitment::getRecruitId) + .collect(Collectors.toSet()); + + // 只插入不存在的记录 + List toInsert = subList.stream() + .filter(r -> !existingIds.contains(r.getRecruitId())) + .collect(Collectors.toList()); + + if (!toInsert.isEmpty()) { + recruitmentMapper.insertBatch(toInsert); + } + } + } + } } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentServiceImpl.java index e58ec8a..be7c655 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentServiceImpl.java @@ -1,6 +1,5 @@ package com.ruoyi.ccdi.service.impl; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.domain.CcdiStaffRecruitment; import com.ruoyi.ccdi.domain.dto.CcdiStaffRecruitmentAddDTO; @@ -10,19 +9,21 @@ import com.ruoyi.ccdi.domain.excel.CcdiStaffRecruitmentExcel; import com.ruoyi.ccdi.domain.vo.CcdiStaffRecruitmentVO; import com.ruoyi.ccdi.enums.AdmitStatus; import com.ruoyi.ccdi.mapper.CcdiStaffRecruitmentMapper; +import com.ruoyi.ccdi.service.ICcdiStaffRecruitmentImportService; import com.ruoyi.ccdi.service.ICcdiStaffRecruitmentService; -import com.ruoyi.common.utils.IdCardUtil; +import com.ruoyi.common.utils.SecurityUtils; 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.UUID; +import java.util.concurrent.TimeUnit; /** * 员工招聘信息 服务层处理 @@ -36,6 +37,12 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer @Resource private CcdiStaffRecruitmentMapper recruitmentMapper; + @Resource + private ICcdiStaffRecruitmentImportService recruitmentImportService; + + @Resource + private RedisTemplate redisTemplate; + /** * 查询招聘信息列表 * @@ -151,149 +158,43 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer } /** - * 导入招聘信息数据(批量优化版本) + * 导入招聘信息数据(异步) * * @param excelList Excel实体列表 - * @return 结果 + * @return 任务ID */ @Override @Transactional - public String importRecruitment(List excelList) { + public String importRecruitment(java.util.List excelList) { if (StringUtils.isNull(excelList) || excelList.isEmpty()) { - return "至少需要一条数据"; + throw new RuntimeException("至少需要一条数据"); } - int successNum = 0; - int failureNum = 0; - StringBuilder successMsg = new StringBuilder(); - StringBuilder failureMsg = new StringBuilder(); + // 生成任务ID + String taskId = UUID.randomUUID().toString(); + long startTime = System.currentTimeMillis(); - // 第一阶段:数据验证和分类 - List toInsertList = new ArrayList<>(); + // 获取当前用户名 + String userName = SecurityUtils.getUsername(); - // 批量收集所有招聘项目编号 - List recruitIds = new ArrayList<>(); + // 初始化Redis状态 + String statusKey = "import:recruitment:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("taskId", taskId); + statusData.put("status", "PROCESSING"); + statusData.put("totalCount", excelList.size()); + statusData.put("successCount", 0); + statusData.put("failureCount", 0); + statusData.put("progress", 0); + statusData.put("startTime", startTime); + statusData.put("message", "正在处理..."); - for (CcdiStaffRecruitmentExcel excel : excelList) { - if (StringUtils.isNotEmpty(excel.getRecruitId())) { - recruitIds.add(excel.getRecruitId()); - } - } + redisTemplate.opsForHash().putAll(statusKey, statusData); + redisTemplate.expire(statusKey, 7, TimeUnit.DAYS); - // 批量查询已存在的招聘项目编号 - Map existingRecruitmentMap = new HashMap<>(); - if (!recruitIds.isEmpty()) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.in(CcdiStaffRecruitment::getRecruitId, recruitIds); - List existingRecruitments = recruitmentMapper.selectList(wrapper); - existingRecruitmentMap = existingRecruitments.stream() - .collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, r -> r)); - } + // 调用异步导入服务 + recruitmentImportService.importRecruitmentAsync(excelList, taskId, userName); - // 第二阶段:处理每条数据 - for (int i = 0; i < excelList.size(); i++) { - CcdiStaffRecruitmentExcel excel = excelList.get(i); - try { - // 验证必填字段和数据格式 - validateRecruitmentDataBasic(excel); - - CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); - BeanUtils.copyProperties(excel, recruitment); - - // 检查是否已存在 - CcdiStaffRecruitment existingRecruitment = existingRecruitmentMap.get(excel.getRecruitId()); - - // 判断数据状态 - if (existingRecruitment != null) { - // 招聘项目编号已存在,直接报错 - throw new RuntimeException(String.format("招聘项目编号[%s]已存在,请勿重复导入", excel.getRecruitId())); - } else { - // 招聘项目编号不存在,新增数据 - recruitment.setCreatedBy("导入"); - toInsertList.add(recruitment); - } - - } catch (Exception e) { - failureNum++; - failureMsg.append("
").append(failureNum).append("、招聘项目编号 ").append(excel.getRecruitId()) - .append(" 导入失败:").append(e.getMessage()); - } - } - - // 第三阶段:批量执行数据库操作 - if (!toInsertList.isEmpty()) { - // 使用自定义批量插入方法 - recruitmentMapper.insertBatch(toInsertList); - successNum += toInsertList.size(); - } - - // 第四阶段:返回结果(只返回错误信息) - if (failureNum > 0) { - failureMsg.insert(0, "很抱歉,导入完成!成功 " + successNum + " 条,失败 " + failureNum + " 条,错误如下:"); - throw new RuntimeException(failureMsg.toString()); - } else { - successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据类型:新增 "); - successMsg.append(toInsertList.size()).append(" 条"); - return successMsg.toString(); - } - } - - /** - * 验证招聘信息数据(仅基本字段验证,不进行数据库查询) - */ - private void validateRecruitmentDataBasic(CcdiStaffRecruitmentExcel excel) { - // 验证必填字段 - if (StringUtils.isEmpty(excel.getRecruitId())) { - throw new RuntimeException("招聘项目编号不能为空"); - } - if (StringUtils.isEmpty(excel.getRecruitName())) { - throw new RuntimeException("招聘项目名称不能为空"); - } - if (StringUtils.isEmpty(excel.getPosName())) { - throw new RuntimeException("职位名称不能为空"); - } - if (StringUtils.isEmpty(excel.getPosCategory())) { - throw new RuntimeException("职位类别不能为空"); - } - if (StringUtils.isEmpty(excel.getPosDesc())) { - throw new RuntimeException("职位描述不能为空"); - } - if (StringUtils.isEmpty(excel.getCandName())) { - throw new RuntimeException("应聘人员姓名不能为空"); - } - if (StringUtils.isEmpty(excel.getCandEdu())) { - throw new RuntimeException("应聘人员学历不能为空"); - } - if (StringUtils.isEmpty(excel.getCandId())) { - throw new RuntimeException("证件号码不能为空"); - } - if (StringUtils.isEmpty(excel.getCandSchool())) { - throw new RuntimeException("应聘人员毕业院校不能为空"); - } - if (StringUtils.isEmpty(excel.getCandMajor())) { - throw new RuntimeException("应聘人员专业不能为空"); - } - if (StringUtils.isEmpty(excel.getCandGrad())) { - throw new RuntimeException("应聘人员毕业年月不能为空"); - } - if (StringUtils.isEmpty(excel.getAdmitStatus())) { - throw new RuntimeException("录用情况不能为空"); - } - - // 验证证件号码格式 - String idCardError = IdCardUtil.getErrorMessage(excel.getCandId()); - if (idCardError != null) { - throw new RuntimeException("证件号码" + idCardError); - } - - // 验证毕业年月格式(YYYYMM) - if (!excel.getCandGrad().matches("^((19|20)\\d{2})(0[1-9]|1[0-2])$")) { - throw new RuntimeException("毕业年月格式不正确,应为YYYYMM"); - } - - // 验证录用状态 - if (AdmitStatus.getDescByCode(excel.getAdmitStatus()) == null) { - throw new RuntimeException("录用情况只能填写'录用'、'未录用'或'放弃'"); - } + return taskId; } } diff --git a/ruoyi-ui/src/api/ccdiStaffRecruitment.js b/ruoyi-ui/src/api/ccdiStaffRecruitment.js index 9cba824..27ec36d 100644 --- a/ruoyi-ui/src/api/ccdiStaffRecruitment.js +++ b/ruoyi-ui/src/api/ccdiStaffRecruitment.js @@ -52,11 +52,30 @@ export function importTemplate() { } // 导入招聘信息 -export function importData(data, updateSupport) { +export function importData(file) { + const formData = new FormData() + formData.append('file', file) return request({ - url: '/ccdi/staffRecruitment/importData?updateSupport=' + updateSupport, + url: '/ccdi/staffRecruitment/importData', method: 'post', - data: data + data: formData + }) +} + +// 查询导入状态 +export function getImportStatus(taskId) { + return request({ + url: '/ccdi/staffRecruitment/importStatus/' + taskId, + method: 'get' + }) +} + +// 查询导入失败记录 +export function getImportFailures(taskId, pageNum, pageSize) { + return request({ + url: '/ccdi/staffRecruitment/importFailures/' + taskId, + method: 'get', + params: { pageNum, pageSize } }) } diff --git a/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue b/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue index 5198486..2a26d06 100644 --- a/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue +++ b/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue @@ -81,6 +81,20 @@ v-hasPermi="['ccdi:staffRecruitment:export']" >导出 + + + 查看导入失败记录 + + @@ -308,7 +322,7 @@ - + + + + + + + + + + + + + + + + + + +