From aa08ab47115d8c28bd07da030a41f8741053d5ab Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Fri, 24 Apr 2026 13:29:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=91=98=E5=B7=A5=E4=BA=B2=E5=B1=9E=E5=AE=9E?= =?UTF-8?q?=E4=BD=93=E5=85=B3=E8=81=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- .../controller/CcdiBaseStaffController.java | 58 +++- .../CcdiStaffFmyRelationController.java | 58 +++- .../CcdiPurchaseTransactionSupplierDTO.java | 14 +- .../domain/vo/AssetImportFailureVO.java | 8 + .../vo/BaseStaffAssetImportFailureVO.java | 8 + .../vo/BaseStaffImportSubmitResultVO.java | 23 ++ .../collection/domain/vo/ImportFailureVO.java | 8 +- .../PurchaseTransactionImportFailureVO.java | 8 + .../vo/StaffFmyRelationImportFailureVO.java | 8 + .../StaffFmyRelationImportSubmitResultVO.java | 24 ++ .../service/ICcdiBaseStaffImportService.java | 5 +- .../service/ICcdiBaseStaffService.java | 5 +- .../impl/CcdiAssetInfoImportServiceImpl.java | 7 +- .../CcdiBaseStaffAssetImportServiceImpl.java | 35 ++- .../impl/CcdiBaseStaffImportServiceImpl.java | 63 ++-- .../impl/CcdiBaseStaffServiceImpl.java | 7 +- ...iPurchaseTransactionImportServiceImpl.java | 263 ++++++++++++---- ...CcdiStaffFmyRelationImportServiceImpl.java | 4 + .../CcdiAssetInfoControllerTest.java | 9 +- ...diBaseStaffAssetImportServiceImplTest.java | 29 ++ .../CcdiBaseStaffImportServiceImplTest.java | 86 ++++- ...ual-sheet-import-backend-implementation.md | 37 +++ ...-failure-display-backend-implementation.md | 26 ++ ...ual-sheet-import-backend-implementation.md | 39 +++ ...-dept-validation-backend-implementation.md | 32 ++ ...plier-validation-backend-implementation.md | 23 ++ ...al-sheet-import-frontend-implementation.md | 38 +++ ...failure-display-frontend-implementation.md | 23 ++ ...al-sheet-import-frontend-implementation.md | 35 +++ ...lier-validation-frontend-implementation.md | 22 ++ ...-staff-dual-sheet-import-implementation.md | 40 +++ ...taff-maintenance-browser-test-execution.md | 51 +++ ...g-import-failure-display-implementation.md | 46 +++ ...family-dual-sheet-import-implementation.md | 48 +++ ...04-23-116-62-17-81-docker-deploy-record.md | 63 ++++ ...f-import-dept-validation-implementation.md | 28 ++ ...ding-supplier-validation-implementation.md | 44 +++ ...-maintenance-search-grid-implementation.md | 188 +++++++++++ .../resources/mapper/system/SysDeptMapper.xml | 4 +- ruoyi-ui/src/api/ccdiBaseStaff.js | 6 +- ruoyi-ui/src/views/ccdiAccountInfo/index.vue | 144 +++++---- ruoyi-ui/src/views/ccdiBaseStaff/index.vue | 294 +++++++----------- .../ccdiCustEnterpriseRelation/index.vue | 91 ++++-- .../src/views/ccdiCustFmyRelation/index.vue | 93 +++--- .../components/SearchForm.vue | 95 +++--- .../views/ccdiPurchaseTransaction/index.vue | 101 +++--- .../src/views/ccdiStaffFmyRelation/index.vue | 266 ++++++---------- .../src/views/ccdiStaffRecruitment/index.vue | 135 ++++---- .../src/views/ccdiStaffTransfer/index.vue | 144 +++++---- ...taff-family-asset-detail-import-ui.test.js | 23 +- 51 files changed, 2070 insertions(+), 845 deletions(-) create mode 100644 ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffImportSubmitResultVO.java create mode 100644 ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportSubmitResultVO.java create mode 100644 docs/plans/backend/2026-04-22-base-staff-dual-sheet-import-backend-implementation.md create mode 100644 docs/plans/backend/2026-04-22-bidding-import-failure-display-backend-implementation.md create mode 100644 docs/plans/backend/2026-04-22-staff-family-dual-sheet-import-backend-implementation.md create mode 100644 docs/plans/backend/2026-04-23-base-staff-import-dept-validation-backend-implementation.md create mode 100644 docs/plans/backend/2026-04-23-bidding-supplier-validation-backend-implementation.md create mode 100644 docs/plans/frontend/2026-04-22-base-staff-dual-sheet-import-frontend-implementation.md create mode 100644 docs/plans/frontend/2026-04-22-bidding-import-failure-display-frontend-implementation.md create mode 100644 docs/plans/frontend/2026-04-22-staff-family-dual-sheet-import-frontend-implementation.md create mode 100644 docs/plans/frontend/2026-04-23-bidding-supplier-validation-frontend-implementation.md create mode 100644 docs/reports/implementation/2026-04-22-base-staff-dual-sheet-import-implementation.md create mode 100644 docs/reports/implementation/2026-04-22-base-staff-maintenance-browser-test-execution.md create mode 100644 docs/reports/implementation/2026-04-22-bidding-import-failure-display-implementation.md create mode 100644 docs/reports/implementation/2026-04-22-staff-family-dual-sheet-import-implementation.md create mode 100644 docs/reports/implementation/2026-04-23-116-62-17-81-docker-deploy-record.md create mode 100644 docs/reports/implementation/2026-04-23-base-staff-import-dept-validation-implementation.md create mode 100644 docs/reports/implementation/2026-04-23-bidding-supplier-validation-implementation.md create mode 100644 docs/reports/implementation/2026-04-23-info-maintenance-search-grid-implementation.md diff --git a/.gitignore b/.gitignore index 9741908d..44700cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,8 @@ ruoyi-ui/vue.config.js tests/ -tongweb_62318.properties \ No newline at end of file +tongweb_62318.properties + +.superpowers/ + +tmp/ \ No newline at end of file diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java index 3f5f877a..a8a358eb 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java @@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO; import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffEditDTO; import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffQueryDTO; +import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel; import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel; import com.ruoyi.info.collection.domain.vo.*; +import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService; import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService; import com.ruoyi.info.collection.service.ICcdiBaseStaffService; import com.ruoyi.info.collection.utils.EasyExcelUtil; @@ -45,6 +47,9 @@ public class CcdiBaseStaffController extends BaseController { @Resource private ICcdiBaseStaffImportService importAsyncService; + @Resource + private ICcdiBaseStaffAssetImportService baseStaffAssetImportService; + /** * 查询员工列表 */ @@ -120,7 +125,14 @@ public class CcdiBaseStaffController extends BaseController { @Operation(summary = "下载导入模板") @PostMapping("/importTemplate") public void importTemplate(HttpServletResponse response) { - EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiBaseStaffExcel.class, "员工信息"); + EasyExcelUtil.importTemplateWithDictDropdown( + response, + CcdiBaseStaffExcel.class, + "员工信息", + CcdiBaseStaffAssetInfoExcel.class, + "员工资产信息", + "员工信息维护导入模板" + ); } /** @@ -130,21 +142,33 @@ public class CcdiBaseStaffController extends BaseController { @PreAuthorize("@ss.hasPermi('ccdi:baseStaff:import')") @Log(title = "员工信息", businessType = BusinessType.IMPORT) @PostMapping("/importData") - public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { - List list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiBaseStaffExcel.class); + public AjaxResult importData(MultipartFile file) throws Exception { + List staffList = EasyExcelUtil.importExcel( + file.getInputStream(), + CcdiBaseStaffExcel.class, + "员工信息" + ); + List assetList = EasyExcelUtil.importExcel( + file.getInputStream(), + CcdiBaseStaffAssetInfoExcel.class, + "员工资产信息" + ); - if (list == null || list.isEmpty()) { + boolean hasStaffRows = staffList != null && !staffList.isEmpty(); + boolean hasAssetRows = assetList != null && !assetList.isEmpty(); + + if (!hasStaffRows && !hasAssetRows) { return error("至少需要一条数据"); } - // 提交异步任务 - String taskId = baseStaffService.importBaseStaff(list, updateSupport); - - // 立即返回,不等待后台任务完成 - ImportResultVO result = new ImportResultVO(); - result.setTaskId(taskId); - result.setStatus("PROCESSING"); - result.setMessage("导入任务已提交,正在后台处理"); + BaseStaffImportSubmitResultVO result = new BaseStaffImportSubmitResultVO(); + if (hasStaffRows) { + result.setStaffTaskId(baseStaffService.importBaseStaff(staffList)); + } + if (hasAssetRows) { + result.setAssetTaskId(baseStaffAssetImportService.importAssetInfo(assetList)); + } + result.setMessage(buildImportSubmitMessage(hasStaffRows, hasAssetRows)); return AjaxResult.success("导入任务已提交,正在后台处理", result); } @@ -190,4 +214,14 @@ public class CcdiBaseStaffController extends BaseController { return getDataTable(pageData, failures.size()); } + + private String buildImportSubmitMessage(boolean hasStaffRows, boolean hasAssetRows) { + if (hasStaffRows && hasAssetRows) { + return "已提交员工信息和员工资产信息导入任务"; + } + if (hasStaffRows) { + return "已提交员工信息导入任务"; + } + return "已提交员工资产信息导入任务"; + } } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java index 59a868c5..1eff571a 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java @@ -4,11 +4,13 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO; import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationEditDTO; import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationQueryDTO; +import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel; import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel; import com.ruoyi.info.collection.domain.vo.CcdiStaffFmyRelationVO; -import com.ruoyi.info.collection.domain.vo.ImportResultVO; import com.ruoyi.info.collection.domain.vo.ImportStatusVO; import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO; +import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO; +import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService; import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService; import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationService; import com.ruoyi.info.collection.utils.EasyExcelUtil; @@ -49,6 +51,9 @@ public class CcdiStaffFmyRelationController extends BaseController { @Resource private ICcdiStaffFmyRelationImportService relationImportService; + @Resource + private ICcdiAssetInfoImportService assetInfoImportService; + /** * 查询员工亲属关系列表 */ @@ -115,7 +120,14 @@ public class CcdiStaffFmyRelationController extends BaseController { @Operation(summary = "下载导入模板") @PostMapping("/importTemplate") public void importTemplate(HttpServletResponse response) { - EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffFmyRelationExcel.class, "员工亲属关系信息"); + EasyExcelUtil.importTemplateWithDictDropdown( + response, + CcdiStaffFmyRelationExcel.class, + "员工亲属关系信息", + CcdiAssetInfoExcel.class, + "亲属资产信息", + "员工亲属关系维护导入模板" + ); } /** @@ -127,20 +139,32 @@ public class CcdiStaffFmyRelationController extends BaseController { @Log(title = "员工亲属关系", businessType = BusinessType.IMPORT) @PostMapping("/importData") public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception { - List list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffFmyRelationExcel.class); + List relationList = EasyExcelUtil.importExcel( + file.getInputStream(), + CcdiStaffFmyRelationExcel.class, + "员工亲属关系信息" + ); + List assetList = EasyExcelUtil.importExcel( + file.getInputStream(), + CcdiAssetInfoExcel.class, + "亲属资产信息" + ); - if (list == null || list.isEmpty()) { + boolean hasRelationRows = relationList != null && !relationList.isEmpty(); + boolean hasAssetRows = assetList != null && !assetList.isEmpty(); + + if (!hasRelationRows && !hasAssetRows) { return error("至少需要一条数据"); } - // 提交异步任务 - String taskId = relationService.importRelation(list); - - // 立即返回,不等待后台任务完成 - ImportResultVO result = new ImportResultVO(); - result.setTaskId(taskId); - result.setStatus("PROCESSING"); - result.setMessage("导入任务已提交,正在后台处理"); + StaffFmyRelationImportSubmitResultVO result = new StaffFmyRelationImportSubmitResultVO(); + if (hasRelationRows) { + result.setRelationTaskId(relationService.importRelation(relationList)); + } + if (hasAssetRows) { + result.setAssetTaskId(assetInfoImportService.importAssetInfo(assetList)); + } + result.setMessage(buildImportSubmitMessage(hasRelationRows, hasAssetRows)); return AjaxResult.success("导入任务已提交,正在后台处理", result); } @@ -186,4 +210,14 @@ public class CcdiStaffFmyRelationController extends BaseController { return getDataTable(pageData, failures.size()); } + + private String buildImportSubmitMessage(boolean hasRelationRows, boolean hasAssetRows) { + if (hasRelationRows && hasAssetRows) { + return "已提交员工亲属关系和亲属资产信息导入任务"; + } + if (hasRelationRows) { + return "已提交员工亲属关系导入任务"; + } + return "已提交亲属资产信息导入任务"; + } } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiPurchaseTransactionSupplierDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiPurchaseTransactionSupplierDTO.java index 5b037908..a828601d 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiPurchaseTransactionSupplierDTO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiPurchaseTransactionSupplierDTO.java @@ -2,8 +2,6 @@ package com.ruoyi.info.collection.domain.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; import lombok.Data; import java.io.Serial; @@ -20,29 +18,19 @@ public class CcdiPurchaseTransactionSupplierDTO implements Serializable { private static final long serialVersionUID = 1L; @NotBlank(message = "供应商名称不能为空") - @Size(max = 200, message = "供应商名称长度不能超过200个字符") @Schema(description = "供应商名称") private String supplierName; - @Pattern( - regexp = "^$|^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$", - message = "供应商统一信用代码格式不正确" - ) + @NotBlank(message = "供应商统一信用代码不能为空") @Schema(description = "供应商统一信用代码") private String supplierUscc; - @Size(max = 50, message = "供应商联系人长度不能超过50个字符") @Schema(description = "供应商联系人") private String contactPerson; - @Pattern( - regexp = "^$|^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$", - message = "供应商联系电话格式不正确" - ) @Schema(description = "供应商联系电话") private String contactPhone; - @Size(max = 50, message = "供应商银行账户长度不能超过50个字符") @Schema(description = "供应商银行账户") private String supplierBankAccount; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AssetImportFailureVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AssetImportFailureVO.java index 4a23b583..ac40a33a 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AssetImportFailureVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AssetImportFailureVO.java @@ -15,6 +15,14 @@ import java.math.BigDecimal; @Schema(description = "亲属资产信息导入失败记录") public class AssetImportFailureVO { + /** Sheet名称 */ + @Schema(description = "Sheet名称") + private String sheetName; + + /** Excel行号 */ + @Schema(description = "Excel行号") + private Integer rowNum; + /** 亲属证件号 */ @Schema(description = "亲属证件号") private String personId; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffAssetImportFailureVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffAssetImportFailureVO.java index 90a7e558..7c53c501 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffAssetImportFailureVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffAssetImportFailureVO.java @@ -15,6 +15,14 @@ import java.math.BigDecimal; @Schema(description = "员工资产信息导入失败记录") public class BaseStaffAssetImportFailureVO { + /** Sheet名称 */ + @Schema(description = "Sheet名称") + private String sheetName; + + /** Excel行号 */ + @Schema(description = "Excel行号") + private Integer rowNum; + /** 员工身份证号 */ @Schema(description = "员工身份证号") private String personId; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffImportSubmitResultVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffImportSubmitResultVO.java new file mode 100644 index 00000000..a7a48551 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffImportSubmitResultVO.java @@ -0,0 +1,23 @@ +package com.ruoyi.info.collection.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 员工双Sheet导入提交结果 + * + * @author ruoyi + */ +@Data +@Schema(description = "员工双Sheet导入提交结果") +public class BaseStaffImportSubmitResultVO { + + @Schema(description = "员工信息导入任务ID") + private String staffTaskId; + + @Schema(description = "员工资产信息导入任务ID") + private String assetTaskId; + + @Schema(description = "提交说明") + private String message; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/ImportFailureVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/ImportFailureVO.java index a401d4f1..bd4d351c 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/ImportFailureVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/ImportFailureVO.java @@ -14,8 +14,14 @@ import java.math.BigDecimal; @Schema(description = "导入失败记录") public class ImportFailureVO { + @Schema(description = "Sheet名称") + private String sheetName; + + @Schema(description = "Excel行号") + private Integer rowNum; + @Schema(description = "柜员号") - private Long employeeId; + private Long staffId; @Schema(description = "姓名") private String name; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/PurchaseTransactionImportFailureVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/PurchaseTransactionImportFailureVO.java index 04deab5d..7e165b90 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/PurchaseTransactionImportFailureVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/PurchaseTransactionImportFailureVO.java @@ -15,6 +15,14 @@ import java.math.BigDecimal; @Schema(description = "招投标信息导入失败记录") public class PurchaseTransactionImportFailureVO { + /** 失败来源Sheet */ + @Schema(description = "失败来源Sheet") + private String sheetName; + + /** 失败行号 */ + @Schema(description = "失败行号") + private String sheetRowNum; + /** 采购事项ID */ @Schema(description = "采购事项ID") private String purchaseId; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportFailureVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportFailureVO.java index 89e7ffe7..b0dcdc26 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportFailureVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportFailureVO.java @@ -15,6 +15,14 @@ import java.math.BigDecimal; @Schema(description = "员工亲属关系信息导入失败记录") public class StaffFmyRelationImportFailureVO { + /** Sheet名称 */ + @Schema(description = "Sheet名称") + private String sheetName; + + /** Excel行号 */ + @Schema(description = "Excel行号") + private Integer rowNum; + /** 员工身份证号 */ @Schema(description = "员工身份证号") private String personId; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportSubmitResultVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportSubmitResultVO.java new file mode 100644 index 00000000..dab16109 --- /dev/null +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportSubmitResultVO.java @@ -0,0 +1,24 @@ +package com.ruoyi.info.collection.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 员工亲属关系双Sheet导入提交结果 + * + * @author ruoyi + * @date 2026-04-22 + */ +@Data +@Schema(description = "员工亲属关系双Sheet导入提交结果") +public class StaffFmyRelationImportSubmitResultVO { + + @Schema(description = "员工亲属关系导入任务ID") + private String relationTaskId; + + @Schema(description = "亲属资产信息导入任务ID") + private String assetTaskId; + + @Schema(description = "提交结果提示") + private String message; +} diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffImportService.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffImportService.java index 8763db2d..a2b15508 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffImportService.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffImportService.java @@ -15,10 +15,9 @@ public interface ICcdiBaseStaffImportService { /** * 异步导入员工数据 * - * @param excelList Excel数据列表 - * @param isUpdateSupport 是否更新已存在的数据 + * @param excelList Excel数据列表 */ - void importBaseStaffAsync(List excelList, Boolean isUpdateSupport, String taskId); + void importBaseStaffAsync(List excelList, String taskId); /** * 查询导入状态 diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffService.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffService.java index 938cf74a..400804e1 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffService.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffService.java @@ -78,11 +78,10 @@ public interface ICcdiBaseStaffService { /** * 导入员工数据 * - * @param excelList Excel实体列表 - * @param isUpdateSupport 是否更新支持 + * @param excelList Excel实体列表 * @return 结果 */ - String importBaseStaff(List excelList, Boolean isUpdateSupport); + String importBaseStaff(List excelList); /** * 查询员工下拉列表 diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java index 30790548..3dc26e1b 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java @@ -39,6 +39,8 @@ import java.util.concurrent.TimeUnit; public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportService { private static final String STATUS_KEY_PREFIX = "import:assetInfo:"; + private static final String SHEET_NAME = "亲属资产信息"; + private static final int EXCEL_DATA_START_ROW = 2; @Resource private CcdiAssetInfoMapper assetInfoMapper; @@ -91,7 +93,8 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi Map> ownerMap = buildOwnerMap(personIds); - for (CcdiAssetInfoExcel excel : excelList) { + for (int i = 0; i < excelList.size(); i++) { + CcdiAssetInfoExcel excel = excelList.get(i); try { validateExcel(excel); Set familyIds = ownerMap.get(excel.getPersonId()); @@ -111,6 +114,8 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi } catch (Exception e) { AssetImportFailureVO failureVO = new AssetImportFailureVO(); BeanUtils.copyProperties(excel, failureVO); + failureVO.setSheetName(SHEET_NAME); + failureVO.setRowNum(i + EXCEL_DATA_START_ROW); failureVO.setErrorMessage(e.getMessage()); failures.add(failureVO); } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java index 1d21ec6f..3f7aa3c4 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java @@ -1,6 +1,7 @@ package com.ruoyi.info.collection.service.impl; import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.info.collection.domain.CcdiAssetInfo; @@ -90,14 +91,24 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI .toList(); Map> ownerMap = buildOwnerMap(personIds); + Set existingAssetKeys = buildExistingAssetKeys(personIds); + Set importedAssetKeys = new java.util.LinkedHashSet<>(); - for (CcdiBaseStaffAssetInfoExcel excel : excelList) { + for (int i = 0; i < excelList.size(); i++) { + CcdiBaseStaffAssetInfoExcel excel = excelList.get(i); try { validateExcel(excel); Set familyIds = ownerMap.get(excel.getPersonId()); if (familyIds == null || familyIds.isEmpty()) { throw new RuntimeException("员工资产导入仅支持员工本人证件号"); } + String assetKey = buildAssetKey(excel.getPersonId(), excel.getAssetMainType(), excel.getAssetSubType(), excel.getAssetName()); + if (existingAssetKeys.contains(assetKey)) { + throw new RuntimeException("资产记录已存在"); + } + if (!importedAssetKeys.add(assetKey)) { + throw new RuntimeException("资产记录在导入文件中重复"); + } CcdiAssetInfo assetInfo = new CcdiAssetInfo(); BeanUtils.copyProperties(excel, assetInfo); @@ -109,6 +120,8 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI } catch (Exception e) { BaseStaffAssetImportFailureVO failureVO = new BaseStaffAssetImportFailureVO(); BeanUtils.copyProperties(excel, failureVO); + failureVO.setSheetName("员工资产信息"); + failureVO.setRowNum(i + 2); failureVO.setErrorMessage(e.getMessage()); failures.add(failureVO); } @@ -168,6 +181,18 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI return result; } + private Set buildExistingAssetKeys(List personIds) { + if (personIds == null || personIds.isEmpty()) { + return Set.of(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(CcdiAssetInfo::getPersonId, personIds); + return assetInfoMapper.selectList(wrapper).stream() + .filter(asset -> StringUtils.equals(asset.getFamilyId(), asset.getPersonId())) + .map(asset -> buildAssetKey(asset.getPersonId(), asset.getAssetMainType(), asset.getAssetSubType(), asset.getAssetName())) + .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new)); + } + private void mergeOwnerMappings(Map> result, List> mappings) { if (mappings == null) { return; @@ -203,6 +228,14 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI } } + private String buildAssetKey(String personId, String assetMainType, String assetSubType, String assetName) { + return String.join("|", + StringUtils.nvl(personId, ""), + StringUtils.nvl(assetMainType, ""), + StringUtils.nvl(assetSubType, ""), + StringUtils.nvl(assetName, "")); + } + private void updateImportStatus(String taskId, String status, ImportResult result) { Map statusData = new HashMap<>(); statusData.put("status", status); diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java index b01cf345..bf594c61 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java @@ -2,6 +2,7 @@ package com.ruoyi.info.collection.service.impl; import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.info.collection.domain.CcdiBaseStaff; import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO; import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel; @@ -13,6 +14,7 @@ import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService; import com.ruoyi.info.collection.utils.ImportLogUtils; import com.ruoyi.common.utils.IdCardUtil; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.mapper.SysDeptMapper; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,16 +45,18 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi @Resource private RedisTemplate redisTemplate; + @Resource + private SysDeptMapper deptMapper; + @Override @Async - public void importBaseStaffAsync(List excelList, Boolean isUpdateSupport, String taskId) { + public void importBaseStaffAsync(List excelList, String taskId) { long startTime = System.currentTimeMillis(); // 记录导入开始 ImportLogUtils.logImportStart(log, taskId, "员工基础信息", excelList.size(), "系统"); List newRecords = new ArrayList<>(); - List updateRecords = new ArrayList<>(); List failures = new ArrayList<>(); // 批量查询已存在的员工ID和身份证号 @@ -75,13 +79,12 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi CcdiBaseStaffAddDTO addDTO = new CcdiBaseStaffAddDTO(); BeanUtils.copyProperties(excel, addDTO); - // 验证数据(支持更新模式) - validateStaffData(addDTO, isUpdateSupport, existingIds, existingIdCards); + validateStaffData(addDTO, existingIds, existingIdCards); CcdiBaseStaff staff = new CcdiBaseStaff(); BeanUtils.copyProperties(excel, staff); - // 统一检查Excel内重复(更新和新增两个分支都需要检查) + // 统一检查Excel内重复 if (processedStaffIds.contains(excel.getStaffId())) { throw new RuntimeException(String.format("员工ID[%d]在导入文件中重复,已跳过此条记录", excel.getStaffId())); } @@ -90,20 +93,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi throw new RuntimeException(String.format("身份证号[%s]在导入文件中重复,已跳过此条记录", excel.getIdCard())); } - // 检查员工ID是否在数据库中已存在 - if (existingIds.contains(excel.getStaffId())) { - // 员工ID已存在于数据库 - if (!isUpdateSupport) { - throw new RuntimeException("员工ID已存在且未启用更新支持"); - } - - // 通过检查,添加到更新列表 - updateRecords.add(staff); - - } else { - // 员工ID不存在,添加到新增列表 - newRecords.add(staff); - } + newRecords.add(staff); // 统一标记为已处理(只有成功添加到列表后才会执行到这里) if (excel.getStaffId() != null) { @@ -115,11 +105,13 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi // 记录进度 ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(), - newRecords.size() + updateRecords.size(), failures.size()); + newRecords.size(), failures.size()); } catch (Exception e) { ImportFailureVO failure = new ImportFailureVO(); BeanUtils.copyProperties(excel, failure); + failure.setSheetName("员工信息"); + failure.setRowNum(i + 2); failure.setErrorMessage(e.getMessage()); failures.add(failure); @@ -137,13 +129,6 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi saveBatch(newRecords, 500); } - // 批量更新已有数据(先删除再插入) - if (!updateRecords.isEmpty() && isUpdateSupport) { - ImportLogUtils.logBatchOperationStart(log, taskId, "更新", - (updateRecords.size() + 499) / 500, 500); - baseStaffMapper.insertOrUpdateBatch(updateRecords); - } - // 保存失败记录到Redis if (!failures.isEmpty()) { try { @@ -157,7 +142,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi ImportResult result = new ImportResult(); result.setTotalCount(excelList.size()); - result.setSuccessCount(newRecords.size() + updateRecords.size()); + result.setSuccessCount(newRecords.size()); result.setFailureCount(failures.size()); // 更新最终状态 @@ -299,11 +284,10 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi * 验证员工数据 * * @param addDTO 新增DTO - * @param isUpdateSupport 是否支持更新 * @param existingIds 已存在的员工ID集合(导入场景使用,传null表示单条新增) * @param existingIdCards 已存在的身份证号集合(导入场景使用,传null表示单条新增) */ - public void validateStaffData(CcdiBaseStaffAddDTO addDTO, Boolean isUpdateSupport, Set existingIds, Set existingIdCards) { + public void validateStaffData(CcdiBaseStaffAddDTO addDTO, Set existingIds, Set existingIdCards) { // 验证必填字段 if (StringUtils.isEmpty(addDTO.getName())) { throw new RuntimeException("姓名不能为空"); @@ -326,6 +310,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi if (StringUtils.isEmpty(addDTO.getStatus())) { throw new RuntimeException("状态不能为空"); } + validateDeptId(addDTO.getDeptId()); // 验证身份证号格式 String idCardError = IdCardUtil.getErrorMessage(addDTO.getIdCard()); @@ -347,12 +332,11 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi throw new RuntimeException("该身份证号已存在"); } } else { - // 导入场景:如果员工ID不存在,才检查身份证号唯一性 - if (!existingIds.contains(addDTO.getStaffId())) { - // 使用批量查询的结果检查身份证号唯一性 - if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) { - throw new RuntimeException("该身份证号已存在"); - } + if (existingIds.contains(addDTO.getStaffId())) { + throw new RuntimeException("该员工ID已存在"); + } + if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) { + throw new RuntimeException("该身份证号已存在"); } } @@ -378,4 +362,11 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi throw new RuntimeException(fieldLabel + "最多保留2位小数"); } } + + private void validateDeptId(Long deptId) { + SysDept dept = deptMapper.selectDeptById(deptId); + if (dept == null || !"0".equals(dept.getStatus()) || !"0".equals(dept.getDelFlag())) { + throw new RuntimeException(String.format("所属部门ID[%d]不存在或已停用/删除,请检查机构号", deptId)); + } + } } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffServiceImpl.java index ac621f59..435e3c66 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffServiceImpl.java @@ -211,13 +211,12 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService { /** * 导入员工数据 * - * @param excelList Excel实体列表 - * @param isUpdateSupport 是否更新支持 + * @param excelList Excel实体列表 * @return 结果 */ @Override @Transactional - public String importBaseStaff(List excelList, Boolean isUpdateSupport) { + public String importBaseStaff(List excelList) { String taskId = UUID.randomUUID().toString(); long startTime = System.currentTimeMillis(); @@ -236,7 +235,7 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService { redisTemplate.opsForHash().putAll(statusKey, statusData); redisTemplate.expire(statusKey, 7, java.util.concurrent.TimeUnit.DAYS); - importAsyncService.importBaseStaffAsync(excelList, isUpdateSupport, taskId); + importAsyncService.importBaseStaffAsync(excelList, taskId); return taskId; } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java index b922f062..2221986a 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java @@ -40,6 +40,9 @@ import java.util.stream.Collectors; public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTransactionImportService { private static final Logger log = LoggerFactory.getLogger(CcdiPurchaseTransactionImportServiceImpl.class); + private static final String MAIN_SHEET_NAME = "招投标主信息"; + private static final String SUPPLIER_SHEET_NAME = "供应商明细"; + private static final int EXCEL_DATA_START_ROW = 2; @Resource private CcdiPurchaseTransactionMapper transactionMapper; @@ -62,6 +65,8 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr long startTime = System.currentTimeMillis(); List safeMainList = mainExcelList == null ? List.of() : mainExcelList; List safeSupplierList = supplierExcelList == null ? List.of() : supplierExcelList; + List indexedMainRows = buildMainImportRows(safeMainList); + List indexedSupplierRows = buildSupplierImportRows(safeSupplierList); int totalCount = countImportUnits(safeMainList, safeSupplierList); // 记录导入开始 @@ -76,17 +81,17 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr Set existingIds = getExistingPurchaseIds(safeMainList); ImportLogUtils.logBatchQueryComplete(log, taskId, "采购事项ID", existingIds.size()); - Map> mainGroupMap = safeMainList.stream() - .filter(item -> StringUtils.isNotEmpty(item.getPurchaseId())) + Map> mainGroupMap = indexedMainRows.stream() + .filter(item -> StringUtils.isNotEmpty(item.data().getPurchaseId())) .collect(Collectors.groupingBy( - CcdiPurchaseTransactionExcel::getPurchaseId, + item -> item.data().getPurchaseId(), LinkedHashMap::new, Collectors.toList() )); - Map> supplierGroupMap = safeSupplierList.stream() - .filter(item -> StringUtils.isNotEmpty(item.getPurchaseId())) + Map> supplierGroupMap = indexedSupplierRows.stream() + .filter(item -> StringUtils.isNotEmpty(item.data().getPurchaseId())) .collect(Collectors.groupingBy( - CcdiPurchaseTransactionSupplierExcel::getPurchaseId, + item -> item.data().getPurchaseId(), LinkedHashMap::new, Collectors.toList() )); @@ -94,34 +99,53 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr purchaseIds.addAll(mainGroupMap.keySet()); purchaseIds.addAll(supplierGroupMap.keySet()); - for (CcdiPurchaseTransactionSupplierExcel supplierExcel : safeSupplierList) { - if (StringUtils.isEmpty(supplierExcel.getPurchaseId())) { - failures.add(buildFailure(null, null, "供应商明细Sheet中的采购事项ID不能为空")); + for (SupplierImportRow supplierExcel : indexedSupplierRows) { + if (StringUtils.isEmpty(supplierExcel.data().getPurchaseId())) { + failures.add(buildFailure( + null, + null, + SUPPLIER_SHEET_NAME, + String.valueOf(supplierExcel.sheetRowNum()), + "供应商明细Sheet中的采购事项ID不能为空" + )); } } int index = 0; for (String purchaseId : purchaseIds) { index++; - List mainRows = mainGroupMap.getOrDefault(purchaseId, List.of()); - List supplierRows = supplierGroupMap.getOrDefault(purchaseId, List.of()); + List mainRows = mainGroupMap.getOrDefault(purchaseId, List.of()); + List supplierRows = supplierGroupMap.getOrDefault(purchaseId, List.of()); try { if (existingIds.contains(purchaseId)) { - throw new RuntimeException(String.format("采购事项ID[%s]已存在,请勿重复导入", purchaseId)); + throw buildValidationException( + MAIN_SHEET_NAME, + extractMainRowNums(mainRows), + String.format("采购事项ID[%s]已存在,请勿重复导入", purchaseId) + ); } if (mainRows.isEmpty()) { - throw new RuntimeException(String.format("采购事项ID[%s]缺少招投标主信息", purchaseId)); + throw buildValidationException( + SUPPLIER_SHEET_NAME, + extractSupplierRowNums(supplierRows), + String.format("采购事项ID[%s]缺少招投标主信息", purchaseId) + ); } if (mainRows.size() > 1) { - throw new RuntimeException(String.format("采购事项ID[%s]在招投标主信息Sheet中重复", purchaseId)); + throw buildValidationException( + MAIN_SHEET_NAME, + extractMainRowNums(mainRows), + String.format("采购事项ID[%s]在招投标主信息Sheet中重复", purchaseId) + ); } - CcdiPurchaseTransactionExcel mainExcel = mainRows.getFirst(); + MainImportRow mainRow = mainRows.getFirst(); + CcdiPurchaseTransactionExcel mainExcel = mainRow.data(); CcdiPurchaseTransactionAddDTO addDTO = new CcdiPurchaseTransactionAddDTO(); BeanUtils.copyProperties(mainExcel, addDTO); - validateTransactionData(addDTO); + validateTransactionData(addDTO, mainRow.sheetRowNum()); List suppliers = buildSupplierEntities(purchaseId, supplierRows, userName); CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction(); @@ -137,8 +161,16 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr newTransactions.size(), failures.size()); } catch (Exception e) { - CcdiPurchaseTransactionExcel mainExcel = mainRows.isEmpty() ? null : mainRows.getFirst(); - failures.add(buildFailure(mainExcel, purchaseId, e.getMessage())); + MainImportRow mainRow = mainRows.isEmpty() ? null : mainRows.getFirst(); + CcdiPurchaseTransactionExcel mainExcel = mainRow == null ? null : mainRow.data(); + FailureMeta failureMeta = resolveFailureMeta(e, mainRows, supplierRows); + failures.add(buildFailure( + mainExcel, + purchaseId, + failureMeta.sheetName(), + failureMeta.sheetRowNum(), + e.getMessage() + )); // 记录验证失败日志 String keyData = String.format("采购事项ID=%s, 采购类别=%s, 标的物=%s", @@ -301,97 +333,99 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr * * @param addDTO 新增DTO */ - private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO) { + private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO, int sheetRowNum) { // 验证必填字段 if (StringUtils.isEmpty(addDTO.getPurchaseId())) { - throw new RuntimeException("采购事项ID不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购事项ID不能为空"); } if (StringUtils.isEmpty(addDTO.getPurchaseCategory())) { - throw new RuntimeException("采购类别不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购类别不能为空"); } if (StringUtils.isEmpty(addDTO.getSubjectName())) { - throw new RuntimeException("标的物名称不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "标的物名称不能为空"); } if (addDTO.getPurchaseQty() == null) { - throw new RuntimeException("采购数量不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购数量不能为空"); } if (addDTO.getBudgetAmount() == null) { - throw new RuntimeException("预算金额不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "预算金额不能为空"); } if (StringUtils.isEmpty(addDTO.getPurchaseMethod())) { - throw new RuntimeException("采购方式不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购方式不能为空"); } if (addDTO.getApplyDate() == null) { - throw new RuntimeException("采购申请日期不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购申请日期不能为空"); } if (StringUtils.isEmpty(addDTO.getApplicantId())) { - throw new RuntimeException("申请人工号不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人工号不能为空"); } if (StringUtils.isEmpty(addDTO.getApplicantName())) { - throw new RuntimeException("申请人姓名不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人姓名不能为空"); } if (StringUtils.isEmpty(addDTO.getApplyDepartment())) { - throw new RuntimeException("申请部门不能为空"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请部门不能为空"); } // 验证工号格式(7位数字) if (!addDTO.getApplicantId().matches("^\\d{7}$")) { - throw new RuntimeException("申请人工号必须为7位数字"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人工号必须为7位数字"); } if (StringUtils.isNotEmpty(addDTO.getPurchaseLeaderId()) && !addDTO.getPurchaseLeaderId().matches("^\\d{7}$")) { - throw new RuntimeException("采购负责人工号必须为7位数字"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购负责人工号必须为7位数字"); } // 验证金额非负 if (addDTO.getPurchaseQty().compareTo(BigDecimal.ZERO) <= 0) { - throw new RuntimeException("采购数量必须大于0"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购数量必须大于0"); } if (addDTO.getBudgetAmount().compareTo(BigDecimal.ZERO) <= 0) { - throw new RuntimeException("预算金额必须大于0"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "预算金额必须大于0"); } if (addDTO.getBidAmount() != null && addDTO.getBidAmount().compareTo(BigDecimal.ZERO) <= 0) { - throw new RuntimeException("中标金额必须大于0"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "中标金额必须大于0"); } if (addDTO.getActualAmount() != null && addDTO.getActualAmount().compareTo(BigDecimal.ZERO) <= 0) { - throw new RuntimeException("实际采购金额必须大于0"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "实际采购金额必须大于0"); } if (addDTO.getContractAmount() != null && addDTO.getContractAmount().compareTo(BigDecimal.ZERO) <= 0) { - throw new RuntimeException("合同金额必须大于0"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "合同金额必须大于0"); } if (addDTO.getSettlementAmount() != null && addDTO.getSettlementAmount().compareTo(BigDecimal.ZERO) <= 0) { - throw new RuntimeException("结算金额必须大于0"); + throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "结算金额必须大于0"); } } private List buildSupplierEntities( String purchaseId, - List supplierRows, + List supplierRows, String userName ) { - List normalizedRows = supplierRows == null + List normalizedRows = supplierRows == null ? List.of() : supplierRows.stream() .filter(Objects::nonNull) - .filter(this::hasAnySupplierValue) + .filter(item -> hasAnySupplierValue(item.data())) .toList(); - long winnerCount = normalizedRows.stream() - .filter(item -> parseIsBidWinner(item.getIsBidWinner()) == 1) - .count(); - if (winnerCount > 1) { - throw new RuntimeException(String.format("采购事项ID[%s]存在多条中标供应商", purchaseId)); - } - - Set duplicateSupplierKeys = new LinkedHashSet<>(); + List winnerRowNums = new ArrayList<>(); + Map supplierKeyRowMap = new LinkedHashMap<>(); List result = new ArrayList<>(); - for (int i = 0; i < normalizedRows.size(); i++) { - CcdiPurchaseTransactionSupplierExcel supplierRow = normalizedRows.get(i); - validateSupplierRow(supplierRow); + for (SupplierImportRow supplierImportRow : normalizedRows) { + CcdiPurchaseTransactionSupplierExcel supplierRow = supplierImportRow.data(); + int isBidWinner = validateSupplierRow(supplierImportRow); + if (isBidWinner == 1) { + winnerRowNums.add(supplierImportRow.sheetRowNum()); + } String duplicateKey = StringUtils.trimToEmpty(supplierRow.getSupplierName()) + "|" + StringUtils.trimToEmpty(supplierRow.getSupplierUscc()); - if (!duplicateSupplierKeys.add(duplicateKey)) { - throw new RuntimeException(String.format("采购事项ID[%s]存在重复供应商", purchaseId)); + Integer firstRowNum = supplierKeyRowMap.putIfAbsent(duplicateKey, supplierImportRow.sheetRowNum()); + if (firstRowNum != null) { + throw buildValidationException( + SUPPLIER_SHEET_NAME, + List.of(firstRowNum, supplierImportRow.sheetRowNum()), + String.format("采购事项ID[%s]存在重复供应商", purchaseId) + ); } CcdiPurchaseTransactionSupplier supplier = new CcdiPurchaseTransactionSupplier(); @@ -401,37 +435,45 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr supplier.setContactPerson(StringUtils.trimToNull(supplierRow.getContactPerson())); supplier.setContactPhone(StringUtils.trimToNull(supplierRow.getContactPhone())); supplier.setSupplierBankAccount(StringUtils.trimToNull(supplierRow.getSupplierBankAccount())); - supplier.setIsBidWinner(parseIsBidWinner(supplierRow.getIsBidWinner())); - supplier.setSortOrder(supplierRow.getSortOrder() == null ? i + 1 : supplierRow.getSortOrder()); + supplier.setIsBidWinner(isBidWinner); + supplier.setSortOrder(supplierRow.getSortOrder() == null ? result.size() + 1 : supplierRow.getSortOrder()); supplier.setCreatedBy(userName); supplier.setUpdatedBy(userName); result.add(supplier); } + if (winnerRowNums.size() > 1) { + throw buildValidationException( + SUPPLIER_SHEET_NAME, + winnerRowNums, + String.format("采购事项ID[%s]存在多条中标供应商", purchaseId) + ); + } return result; } - private void validateSupplierRow(CcdiPurchaseTransactionSupplierExcel supplierRow) { + private int validateSupplierRow(SupplierImportRow supplierImportRow) { + CcdiPurchaseTransactionSupplierExcel supplierRow = supplierImportRow.data(); if (StringUtils.isEmpty(supplierRow.getSupplierName())) { - throw new RuntimeException("供应商名称不能为空"); + throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商名称不能为空"); } if (StringUtils.length(supplierRow.getSupplierName()) > 200) { - throw new RuntimeException("供应商名称长度不能超过200个字符"); + throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商名称长度不能超过200个字符"); } if (StringUtils.length(supplierRow.getContactPerson()) > 50) { - throw new RuntimeException("供应商联系人长度不能超过50个字符"); + throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商联系人长度不能超过50个字符"); } if (StringUtils.length(supplierRow.getSupplierBankAccount()) > 50) { - throw new RuntimeException("供应商银行账户长度不能超过50个字符"); + throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商银行账户长度不能超过50个字符"); } if (StringUtils.isNotEmpty(supplierRow.getContactPhone()) && !supplierRow.getContactPhone().matches("^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$")) { - throw new RuntimeException("供应商联系电话格式不正确"); + throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商联系电话格式不正确"); } if (StringUtils.isNotEmpty(supplierRow.getSupplierUscc()) && !supplierRow.getSupplierUscc().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) { - throw new RuntimeException("供应商统一信用代码格式不正确"); + throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商统一信用代码格式不正确"); } - parseIsBidWinner(supplierRow.getIsBidWinner()); + return parseIsBidWinner(supplierRow.getIsBidWinner(), supplierImportRow.sheetRowNum()); } private boolean hasAnySupplierValue(CcdiPurchaseTransactionSupplierExcel supplierRow) { @@ -445,7 +487,7 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr || supplierRow.getSortOrder() != null; } - private int parseIsBidWinner(String rawValue) { + private int parseIsBidWinner(String rawValue, Integer sheetRowNum) { if (StringUtils.isEmpty(rawValue)) { return 0; } @@ -458,7 +500,11 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr || "FALSE".equalsIgnoreCase(normalized)) { return 0; } - throw new RuntimeException("是否中标仅支持填写“是/否”或“1/0”"); + throw buildValidationException( + SUPPLIER_SHEET_NAME, + sheetRowNum == null ? List.of() : List.of(sheetRowNum), + "是否中标仅支持填写“是/否”或“1/0”" + ); } private void fillWinnerSummary( @@ -487,12 +533,16 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr private PurchaseTransactionImportFailureVO buildFailure( CcdiPurchaseTransactionExcel mainExcel, String purchaseId, + String sheetName, + String sheetRowNum, String errorMessage ) { PurchaseTransactionImportFailureVO failure = new PurchaseTransactionImportFailureVO(); if (mainExcel != null) { BeanUtils.copyProperties(mainExcel, failure); } + failure.setSheetName(sheetName); + failure.setSheetRowNum(sheetRowNum); if (StringUtils.isNotEmpty(purchaseId)) { failure.setPurchaseId(purchaseId); } @@ -519,4 +569,87 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr ); return purchaseIds.size(); } + + private List buildMainImportRows(List mainExcelList) { + List rows = new ArrayList<>(); + for (int i = 0; i < mainExcelList.size(); i++) { + rows.add(new MainImportRow(mainExcelList.get(i), i + EXCEL_DATA_START_ROW)); + } + return rows; + } + + private List buildSupplierImportRows(List supplierExcelList) { + List rows = new ArrayList<>(); + for (int i = 0; i < supplierExcelList.size(); i++) { + rows.add(new SupplierImportRow(supplierExcelList.get(i), i + EXCEL_DATA_START_ROW)); + } + return rows; + } + + private List extractMainRowNums(List rows) { + return rows.stream().map(MainImportRow::sheetRowNum).toList(); + } + + private List extractSupplierRowNums(List rows) { + return rows.stream().map(SupplierImportRow::sheetRowNum).toList(); + } + + private ImportValidationException buildValidationException(String sheetName, List rowNums, String message) { + return new ImportValidationException(sheetName, formatSheetRowNum(rowNums), message); + } + + private FailureMeta resolveFailureMeta( + Exception exception, + List mainRows, + List supplierRows + ) { + if (exception instanceof ImportValidationException validationException) { + return new FailureMeta(validationException.getSheetName(), validationException.getSheetRowNum()); + } + if (!mainRows.isEmpty()) { + return new FailureMeta(MAIN_SHEET_NAME, formatSheetRowNum(extractMainRowNums(mainRows))); + } + if (!supplierRows.isEmpty()) { + return new FailureMeta(SUPPLIER_SHEET_NAME, formatSheetRowNum(extractSupplierRowNums(supplierRows))); + } + return new FailureMeta("", ""); + } + + private String formatSheetRowNum(List rowNums) { + if (rowNums == null || rowNums.isEmpty()) { + return ""; + } + return rowNums.stream() + .filter(Objects::nonNull) + .distinct() + .sorted() + .map(String::valueOf) + .collect(Collectors.joining("、")); + } + + private record MainImportRow(CcdiPurchaseTransactionExcel data, int sheetRowNum) {} + + private record SupplierImportRow(CcdiPurchaseTransactionSupplierExcel data, int sheetRowNum) {} + + private record FailureMeta(String sheetName, String sheetRowNum) {} + + private static class ImportValidationException extends RuntimeException { + + private final String sheetName; + private final String sheetRowNum; + + private ImportValidationException(String sheetName, String sheetRowNum, String message) { + super(message); + this.sheetName = sheetName; + this.sheetRowNum = sheetRowNum; + } + + public String getSheetName() { + return sheetName; + } + + public String getSheetRowNum() { + return sheetRowNum; + } + } } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java index f8c16c78..5d7e3588 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java @@ -41,6 +41,8 @@ import java.util.stream.Collectors; public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelationImportService { private static final Logger log = LoggerFactory.getLogger(CcdiStaffFmyRelationImportServiceImpl.class); + private static final String SHEET_NAME = "员工亲属关系信息"; + private static final int EXCEL_DATA_START_ROW = 2; @Resource private CcdiStaffFmyRelationMapper relationMapper; @@ -168,6 +170,8 @@ public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelat } catch (Exception e) { StaffFmyRelationImportFailureVO failure = new StaffFmyRelationImportFailureVO(); BeanUtils.copyProperties(excel, failure); + failure.setSheetName(SHEET_NAME); + failure.setRowNum(i + EXCEL_DATA_START_ROW); failure.setErrorMessage(e.getMessage()); failures.add(failure); diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiAssetInfoControllerTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiAssetInfoControllerTest.java index 79b145f7..a2bd74ac 100644 --- a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiAssetInfoControllerTest.java +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiAssetInfoControllerTest.java @@ -96,8 +96,12 @@ class CcdiAssetInfoControllerTest { @Test void getImportFailures_shouldReturnPagedRows() { AssetImportFailureVO failure1 = new AssetImportFailureVO(); + failure1.setSheetName("亲属资产信息"); + failure1.setRowNum(2); failure1.setPersonId("A1"); AssetImportFailureVO failure2 = new AssetImportFailureVO(); + failure2.setSheetName("亲属资产信息"); + failure2.setRowNum(3); failure2.setPersonId("A2"); when(assetInfoImportService.getImportFailures("task-3")).thenReturn(List.of(failure1, failure2)); @@ -105,7 +109,10 @@ class CcdiAssetInfoControllerTest { assertEquals(2, result.getTotal()); assertEquals(1, result.getRows().size()); - assertEquals("A2", ((AssetImportFailureVO) result.getRows().get(0)).getPersonId()); + AssetImportFailureVO row = (AssetImportFailureVO) result.getRows().get(0); + assertEquals("亲属资产信息", row.getSheetName()); + assertEquals(3, row.getRowNum()); + assertEquals("A2", row.getPersonId()); } @Test diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffAssetImportServiceImplTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffAssetImportServiceImplTest.java index 09349a9a..593d757a 100644 --- a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffAssetImportServiceImplTest.java +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffAssetImportServiceImplTest.java @@ -93,10 +93,39 @@ class CcdiBaseStaffAssetImportServiceImplTest { ArgumentCaptor failureCaptor = ArgumentCaptor.forClass(Object.class); verify(valueOperations).set(eq("import:baseStaffAsset:task-2:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS)); BaseStaffAssetImportFailureVO failure = (BaseStaffAssetImportFailureVO) ((List) failureCaptor.getValue()).get(0); + assertEquals("员工资产信息", failure.getSheetName()); + assertEquals(2, failure.getRowNum()); assertEquals("320101199201010022", failure.getPersonId()); assertTrue(failure.getErrorMessage().contains("员工资产导入仅支持员工本人证件号")); } + @Test + void importAssetInfoAsync_shouldFailWhenAssetAlreadyExists() { + CcdiBaseStaffAssetInfoExcel excel = buildExcel("320101199001010011", "房产"); + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(assetInfoMapper.selectOwnerCandidatesByBaseStaffIdCards(List.of("320101199001010011"))) + .thenReturn(List.of(owner("320101199001010011", "320101199001010011"))); + + CcdiAssetInfo existing = new CcdiAssetInfo(); + existing.setFamilyId("320101199001010011"); + existing.setPersonId("320101199001010011"); + existing.setAssetMainType("房产"); + existing.setAssetSubType("房产小类"); + existing.setAssetName("房产名称"); + when(assetInfoMapper.selectList(any())).thenReturn(List.of(existing)); + + service.importAssetInfoAsync(List.of(excel), "task-duplicate", "tester"); + + verify(assetInfoMapper, never()).insertBatch(any()); + ArgumentCaptor failureCaptor = ArgumentCaptor.forClass(Object.class); + verify(valueOperations).set(eq("import:baseStaffAsset:task-duplicate:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS)); + BaseStaffAssetImportFailureVO failure = (BaseStaffAssetImportFailureVO) ((List) failureCaptor.getValue()).get(0); + assertEquals("员工资产信息", failure.getSheetName()); + assertEquals(2, failure.getRowNum()); + assertTrue(failure.getErrorMessage().contains("资产记录已存在")); + } + @Test void getImportStatusAndFailures_shouldUseBaseStaffAssetPrefixes() { when(redisTemplate.opsForHash()).thenReturn(hashOperations); diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffImportServiceImplTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffImportServiceImplTest.java index f06a3c7f..6950d605 100644 --- a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffImportServiceImplTest.java +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffImportServiceImplTest.java @@ -1,8 +1,14 @@ package com.ruoyi.info.collection.service; +import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO; import com.ruoyi.info.collection.service.impl.CcdiBaseStaffImportServiceImpl; +import com.ruoyi.system.mapper.SysDeptMapper; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import java.math.BigDecimal; import java.util.Collections; @@ -11,60 +17,109 @@ import java.util.Set; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; +@ExtendWith(MockitoExtension.class) class CcdiBaseStaffImportServiceImplTest { - private final CcdiBaseStaffImportServiceImpl service = new CcdiBaseStaffImportServiceImpl(); + @InjectMocks + private CcdiBaseStaffImportServiceImpl service; + + @Mock + private SysDeptMapper deptMapper; @Test void validateStaffData_shouldAllowEmptyAnnualIncome() { - assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), false, Collections.emptySet(), Collections.emptySet())); + mockNormalDept(); + assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), Collections.emptySet(), Collections.emptySet())); } @Test void validateStaffData_shouldAllowZeroAndTwoDecimalAnnualIncome() { - assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("0.00")), false, Collections.emptySet(), Collections.emptySet())); - assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("12345.67")), false, Collections.emptySet(), Collections.emptySet())); + mockNormalDept(); + assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("0.00")), Collections.emptySet(), Collections.emptySet())); + assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("12345.67")), Collections.emptySet(), Collections.emptySet())); } @Test void validateStaffData_shouldAllowPartyMemberValuesZeroAndOne() { + mockNormalDept(); CcdiBaseStaffAddDTO nonPartyMember = buildDto(null); nonPartyMember.setPartyMember(0); CcdiBaseStaffAddDTO partyMember = buildDto(null); partyMember.setPartyMember(1); - assertDoesNotThrow(() -> service.validateStaffData(nonPartyMember, false, Collections.emptySet(), Collections.emptySet())); - assertDoesNotThrow(() -> service.validateStaffData(partyMember, false, Collections.emptySet(), Collections.emptySet())); + assertDoesNotThrow(() -> service.validateStaffData(nonPartyMember, Collections.emptySet(), Collections.emptySet())); + assertDoesNotThrow(() -> service.validateStaffData(partyMember, Collections.emptySet(), Collections.emptySet())); } @Test void validateStaffData_shouldRejectInvalidPartyMemberValue() { + mockNormalDept(); CcdiBaseStaffAddDTO dto = buildDto(null); dto.setPartyMember(2); RuntimeException exception = assertThrows(RuntimeException.class, - () -> service.validateStaffData(dto, false, Set.of(), Set.of())); + () -> service.validateStaffData(dto, Set.of(), Set.of())); assertEquals("是否党员只能填写'0'或'1'", exception.getMessage()); } @Test void validateStaffData_shouldRejectNegativeAnnualIncome() { + mockNormalDept(); RuntimeException exception = assertThrows(RuntimeException.class, - () -> service.validateStaffData(buildDto(new BigDecimal("-1.00")), false, Set.of(), Set.of())); + () -> service.validateStaffData(buildDto(new BigDecimal("-1.00")), Set.of(), Set.of())); assertEquals("年收入不能为负数", exception.getMessage()); } @Test void validateStaffData_shouldRejectAnnualIncomeWithMoreThanTwoDecimals() { + mockNormalDept(); RuntimeException exception = assertThrows(RuntimeException.class, - () -> service.validateStaffData(buildDto(new BigDecimal("12.345")), false, Set.of(), Set.of())); + () -> service.validateStaffData(buildDto(new BigDecimal("12.345")), Set.of(), Set.of())); assertEquals("年收入最多保留2位小数", exception.getMessage()); } + @Test + void validateStaffData_shouldAllowWhenDeptIsNormalAndNotDeleted() { + mockNormalDept(); + assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), Set.of(), Set.of())); + } + + @Test + void validateStaffData_shouldRejectWhenDeptDoesNotExist() { + when(deptMapper.selectDeptById(10L)).thenReturn(null); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> service.validateStaffData(buildDto(null), Set.of(), Set.of())); + + assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage()); + } + + @Test + void validateStaffData_shouldRejectWhenDeptIsDisabled() { + when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("1", "0")); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> service.validateStaffData(buildDto(null), Set.of(), Set.of())); + + assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage()); + } + + @Test + void validateStaffData_shouldRejectWhenDeptIsDeleted() { + when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("0", "2")); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> service.validateStaffData(buildDto(null), Set.of(), Set.of())); + + assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage()); + } + private CcdiBaseStaffAddDTO buildDto(BigDecimal annualIncome) { CcdiBaseStaffAddDTO dto = new CcdiBaseStaffAddDTO(); dto.setName("张三"); @@ -77,4 +132,17 @@ class CcdiBaseStaffImportServiceImplTest { dto.setAnnualIncome(annualIncome); return dto; } + + private SysDept buildDept(String status, String delFlag) { + SysDept dept = new SysDept(); + dept.setDeptId(10L); + dept.setDeptName("测试部门"); + dept.setStatus(status); + dept.setDelFlag(delFlag); + return dept; + } + + private void mockNormalDept() { + lenient().when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("0", "0")); + } } diff --git a/docs/plans/backend/2026-04-22-base-staff-dual-sheet-import-backend-implementation.md b/docs/plans/backend/2026-04-22-base-staff-dual-sheet-import-backend-implementation.md new file mode 100644 index 00000000..019df245 --- /dev/null +++ b/docs/plans/backend/2026-04-22-base-staff-dual-sheet-import-backend-implementation.md @@ -0,0 +1,37 @@ +# 员工信息维护双 Sheet 导入后端实施计划 + +## 目标 +- 将员工信息维护导入模板改为 `员工信息` + `员工资产信息` 双 Sheet。 +- 统一由 `/ccdi/baseStaff/importData` 接收单文件上传,并按有数据的 Sheet 分别调用现有员工导入与员工资产导入方法。 +- 员工信息导入取消“更新已存在员工”能力,命中现有员工 ID 或身份证号时直接记失败。 +- 两类失败记录统一补充 `sheetName`、`rowNum`、`errorMessage`,便于直接定位 Excel 中的失败位置。 + +## 实施内容 +- 控制器改造 + - 修改 `CcdiBaseStaffController#importTemplate`,下载双 Sheet 模板,文件名统一为“员工信息维护导入模板”。 + - 修改 `CcdiBaseStaffController#importData`,按 Sheet 名分别读取 `CcdiBaseStaffExcel` 与 `CcdiBaseStaffAssetInfoExcel`。 + - 两个 Sheet 均为空时返回错误;任一 Sheet 有数据时,仅提交对应导入任务。 + - 返回新的双任务提交结果对象,包含 `staffTaskId`、`assetTaskId`、`message`。 +- 服务改造 + - 修改 `ICcdiBaseStaffService`、`CcdiBaseStaffServiceImpl`,移除 `updateSupport` 参数。 + - 修改 `ICcdiBaseStaffImportService`、`CcdiBaseStaffImportServiceImpl`,移除更新分支与 `insertOrUpdateBatch` 调用。 + - 员工导入校验统一为: + - 员工 ID 已存在:失败 + - 身份证号已存在:失败 + - Excel 内重复:失败 + - 员工资产导入补充重复校验: + - 数据库中存在同一 `personId + assetMainType + assetSubType + assetName`:失败 + - 导入文件中存在同一组合重复:失败 +- VO 修正 + - 新增员工双 Sheet 提交结果 VO。 + - 修正员工导入失败记录 VO 字段名为 `staffId`,与前端表格字段保持一致。 + - 员工与员工资产失败记录 VO 均增加 `sheetName`、`rowNum`。 + +## 验证 +- `mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile` +- 补充控制器与服务层回归测试,覆盖双 Sheet 分发与“已存在即失败”规则。 + +## 影响范围 +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/` +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/` +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/` diff --git a/docs/plans/backend/2026-04-22-bidding-import-failure-display-backend-implementation.md b/docs/plans/backend/2026-04-22-bidding-import-failure-display-backend-implementation.md new file mode 100644 index 00000000..c46ed7e1 --- /dev/null +++ b/docs/plans/backend/2026-04-22-bidding-import-failure-display-backend-implementation.md @@ -0,0 +1,26 @@ +# 2026-04-22 招投标导入失败展示增强后端实施计划 + +## 1. 目标 + +- 为招投标导入失败记录补充失败来源 `Sheet` +- 为失败记录补充 Excel 失败行号 +- 保持现有导入校验逻辑不变,仅增强失败记录元数据 + +## 2. 涉及范围 + +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/PurchaseTransactionImportFailureVO.java` +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java` + +## 3. 实施步骤 + +1. 在失败记录 VO 中新增 `sheetName`、`sheetRowNum` 字段,供前端弹窗直接读取 +2. 在导入服务中为主信息 Sheet 和供应商明细 Sheet 建立“Excel 数据行号”上下文 +3. 在主信息校验、供应商校验、主从关系校验、空采购事项 ID 供应商校验等失败分支中,统一写入对应的 `Sheet` 与行号 +4. 对跨多行触发的失败场景,行号以合并字符串形式返回,便于页面直接展示 +5. 保留原有失败原因与业务字段,避免影响已有失败记录查询接口 + +## 4. 验证方式 + +- 执行后端编译,确认新增字段和异常封装无编译错误 +- 通过真实页面上传失败样本,核对失败记录接口返回 `sheetName / sheetRowNum / errorMessage` +- 覆盖至少一个主信息失败样本和一个供应商明细失败样本 diff --git a/docs/plans/backend/2026-04-22-staff-family-dual-sheet-import-backend-implementation.md b/docs/plans/backend/2026-04-22-staff-family-dual-sheet-import-backend-implementation.md new file mode 100644 index 00000000..7e10bc56 --- /dev/null +++ b/docs/plans/backend/2026-04-22-staff-family-dual-sheet-import-backend-implementation.md @@ -0,0 +1,39 @@ +# 员工亲属关系维护双 Sheet 导入后端实施计划 + +## 目标 +- 将员工亲属关系维护导入模板改为双 Sheet: + - `员工亲属关系信息` + - `亲属资产信息` +- 将导入提交入口统一到 `/ccdi/staffFmyRelation/importData`。 +- 统一补充失败记录定位字段,支持前端展示 `Sheet / Excel行号 / 失败原因`。 + +## 实施内容 +- Controller 调整 + - `CcdiStaffFmyRelationController#importTemplate` 改为输出双 Sheet 模板,模板文件名统一为“员工亲属关系维护导入模板”。 + - `CcdiStaffFmyRelationController#importData` 一次读取两个 Sheet。 + - 按有数据的 Sheet 分别提交亲属关系导入任务和亲属资产导入任务。 + - 返回新的提交结果 VO,包含 `relationTaskId`、`assetTaskId` 和提示文案。 +- VO 调整 + - `StaffFmyRelationImportFailureVO` 增加 `sheetName`、`rowNum`。 + - `AssetImportFailureVO` 增加 `sheetName`、`rowNum`。 + - 新增 `StaffFmyRelationImportSubmitResultVO`。 +- 导入服务调整 + - `CcdiStaffFmyRelationImportServiceImpl` 失败记录写入固定 `sheetName=员工亲属关系信息`,并记录 Excel 数据行号。 + - `CcdiAssetInfoImportServiceImpl` 失败记录写入固定 `sheetName=亲属资产信息`,并记录 Excel 数据行号。 +- 兼容策略 + - 保留原 `CcdiAssetInfoController` 的状态查询与失败记录查询接口,前端继续复用原有资产任务轮询与失败记录查看能力。 + +## 验证 +- 后端优先验证: + - `CcdiStaffFmyRelationControllerTest` + - `CcdiAssetInfoControllerTest` +- 编译验证: + - `mvn -pl ccdi-info-collection -am -Dmaven.test.skip=true compile` + +## 影响范围 +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java` +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java` +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java` +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportFailureVO.java` +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AssetImportFailureVO.java` +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/StaffFmyRelationImportSubmitResultVO.java` diff --git a/docs/plans/backend/2026-04-23-base-staff-import-dept-validation-backend-implementation.md b/docs/plans/backend/2026-04-23-base-staff-import-dept-validation-backend-implementation.md new file mode 100644 index 00000000..8ee5ebca --- /dev/null +++ b/docs/plans/backend/2026-04-23-base-staff-import-dept-validation-backend-implementation.md @@ -0,0 +1,32 @@ +# 员工信息导入机构号校验后端实施计划 + +## 目标 +- 在员工信息 Excel 导入链路中校验 `所属部门ID(deptId)` 是否对应有效机构号。 +- 有效口径统一为 `sys_dept` 中“正常且未删除”的部门,即 `status = '0'` 且 `del_flag = '0'`。 +- 命中不存在、已停用或已删除的部门时,不入库,直接进入员工导入失败记录。 + +## 实施内容 +- 导入服务改造 + - 修改 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java`。 + - 在 `validateStaffData` 中于必填校验后增加 `deptId` 有效性校验。 + - 新增私有方法按 `deptId` 查询部门并校验 `status` 与 `delFlag`。 + - 校验失败时抛出统一错误文案:`所属部门ID[xxx]不存在或已停用/删除,请检查机构号`。 +- 部门 Mapper 对齐 + - 修改 `ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml`。 + - 为 `selectDeptById` 查询补齐 `d.del_flag` 字段,保证导入服务可同时判断停用与逻辑删除状态。 +- 单元测试补充 + - 修改 `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffImportServiceImplTest.java`。 + - 增加部门存在、停用、删除三类校验测试。 + - 修改 `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffDualImportServiceTest.java`。 + - 增加混合导入场景测试,验证合法员工成功入库、非法 `deptId` 写入失败记录且任务状态为 `PARTIAL_SUCCESS`。 + +## 验证 +- 定向单测: + - `mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiBaseStaffImportServiceImplTest,CcdiBaseStaffDualImportServiceTest test` +- 编译校验: + - `mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile` + +## 影响范围 +- 员工信息导入后端异步校验逻辑 +- 系统部门主键查询字段映射 +- 员工导入相关单元测试 diff --git a/docs/plans/backend/2026-04-23-bidding-supplier-validation-backend-implementation.md b/docs/plans/backend/2026-04-23-bidding-supplier-validation-backend-implementation.md new file mode 100644 index 00000000..6164d867 --- /dev/null +++ b/docs/plans/backend/2026-04-23-bidding-supplier-validation-backend-implementation.md @@ -0,0 +1,23 @@ +# 招投标供应商校验后端实施计划 + +## 目标 +- 让招投标信息维护页面的新增、编辑接口仅保留供应商名称和统一信用代码必填校验。 +- 移除供应商联系人、联系电话、银行账户,以及供应商名称/统一信用代码的内容格式校验,避免页面保存被接口层拦截。 + +## 实施内容 +- 调整 `CcdiPurchaseTransactionSupplierDTO` + - 保留 `supplierName` 的 `@NotBlank`。 + - 为 `supplierUscc` 增加 `@NotBlank` 必填校验。 + - 移除 `supplierName` 的长度校验。 + - 移除 `supplierUscc` 的格式校验。 + - 移除 `contactPerson`、`contactPhone`、`supplierBankAccount` 的内容校验注解。 + +## 验证 +- `mvn -pl ccdi-info-collection -am -DskipTests compile` +- `sh bin/restart_java_backend.sh` +- 结合真实页面验证: + - 新增弹窗提交 `supplierUscc=ABC`、`contactPhone=123` 成功 + - 编辑弹窗提交 `supplierUscc=XYZ`、`contactPhone=abc123` 成功 + +## 产出文件 +- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiPurchaseTransactionSupplierDTO.java` diff --git a/docs/plans/frontend/2026-04-22-base-staff-dual-sheet-import-frontend-implementation.md b/docs/plans/frontend/2026-04-22-base-staff-dual-sheet-import-frontend-implementation.md new file mode 100644 index 00000000..a66e4fc7 --- /dev/null +++ b/docs/plans/frontend/2026-04-22-base-staff-dual-sheet-import-frontend-implementation.md @@ -0,0 +1,38 @@ +# 员工信息维护双 Sheet 导入前端实施计划 + +## 目标 +- 将员工信息维护页面的两个导入按钮合并为一个。 +- 统一使用双 Sheet 模板上传,并根据后端返回的 `staffTaskId`、`assetTaskId` 分别沿用原有轮询与失败记录能力。 +- 保留员工失败记录与员工资产失败记录两个独立查看入口。 +- 两个失败记录弹窗都需要展示失败来源 Sheet、Excel 行号和失败原因。 + +## 实施内容 +- 页面入口调整 + - 删除“导入资产信息”按钮,仅保留一个“导入”按钮。 + - 删除独立员工资产上传弹窗,保留一个统一上传弹窗。 +- 上传交互调整 + - 去掉“是否更新已经存在的员工数据”复选框。 + - 模板提示调整为双 Sheet 说明,明确支持只填一个或同时填写两个 Sheet。 + - 下载模板文件名统一为“员工信息维护导入模板”。 +- 任务处理调整 + - 上传成功后解析 `staffTaskId`、`assetTaskId`。 + - 有员工任务 ID 时,启动原员工导入轮询。 + - 有资产任务 ID 时,启动原资产导入轮询。 + - 未返回的任务类型不清空对应历史失败记录状态。 +- 失败记录展示调整 + - 员工失败记录弹窗增加 `Sheet`、`Excel行号` 列。 + - 员工资产失败记录弹窗增加 `Sheet`、`Excel行号` 列。 +- API 调整 + - `ruoyi-ui/src/api/ccdiBaseStaff.js` 去掉 `updateSupport` 参数,保持单文件上传定义。 + +## 验证 +- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod` +- 页面联调覆盖: + - 只导员工 Sheet + - 只导资产 Sheet + - 双 Sheet 同时导入 + - 两类失败记录入口分别展示 + +## 影响范围 +- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` +- `ruoyi-ui/src/api/ccdiBaseStaff.js` diff --git a/docs/plans/frontend/2026-04-22-bidding-import-failure-display-frontend-implementation.md b/docs/plans/frontend/2026-04-22-bidding-import-failure-display-frontend-implementation.md new file mode 100644 index 00000000..0551a2b2 --- /dev/null +++ b/docs/plans/frontend/2026-04-22-bidding-import-failure-display-frontend-implementation.md @@ -0,0 +1,23 @@ +# 2026-04-22 招投标导入失败展示增强前端实施计划 + +## 1. 目标 + +- 调整招投标信息维护页面的“导入失败记录”弹窗 +- 让失败列表直接展示失败来源 `Sheet`、失败行号、失败原因 + +## 2. 涉及范围 + +- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue` + +## 3. 实施步骤 + +1. 在失败记录弹窗表格中新增 `失败Sheet` 列 +2. 在失败记录弹窗表格中新增 `失败行号` 列 +3. 保留现有 `采购事项ID / 项目名称 / 标的物名称 / 失败原因` 上下文信息 +4. 对多行场景直接展示后端返回的合并行号,不在前端重复解析 + +## 4. 验证方式 + +- 使用真实页面上传失败样本 +- 打开“查看导入失败记录”弹窗,核对主信息失败能显示 `招投标主信息 + 行号 + 原因` +- 核对供应商失败能显示 `供应商明细 + 行号 + 原因` diff --git a/docs/plans/frontend/2026-04-22-staff-family-dual-sheet-import-frontend-implementation.md b/docs/plans/frontend/2026-04-22-staff-family-dual-sheet-import-frontend-implementation.md new file mode 100644 index 00000000..9e9c4c6c --- /dev/null +++ b/docs/plans/frontend/2026-04-22-staff-family-dual-sheet-import-frontend-implementation.md @@ -0,0 +1,35 @@ +# 员工亲属关系维护双 Sheet 导入前端实施计划 + +## 目标 +- 将员工亲属关系维护页面顶部两个导入按钮合并为一个。 +- 上传弹窗改为双 Sheet 提示和统一模板下载。 +- 保留原有两套任务轮询与失败记录入口,但失败记录列表统一展示 `Sheet / Excel行号 / 失败原因`。 + +## 实施内容 +- 页面入口调整 + - 删除“导入亲属资产信息”独立按钮。 + - 删除独立资产上传弹窗,仅保留统一导入弹窗。 +- 上传交互调整 + - 导入弹窗提示模板包含 `员工亲属关系信息` 和 `亲属资产信息` 两个 Sheet。 + - 下载模板文件名统一为“员工亲属关系维护导入模板”。 + - 上传成功后解析后端返回的 `relationTaskId`、`assetTaskId`。 +- 状态管理调整 + - 有 `relationTaskId` 时沿用原亲属关系任务轮询与失败记录缓存。 + - 有 `assetTaskId` 时沿用原亲属资产任务轮询与失败记录缓存。 + - 未返回的任务类型不主动清空既有历史失败记录状态。 +- 失败记录展示调整 + - 亲属关系失败记录弹窗新增 `Sheet`、`Excel行号` 列。 + - 亲属资产失败记录弹窗新增 `Sheet`、`Excel行号` 列。 + +## 验证 +- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && node tests/unit/staff-family-asset-detail-import-ui.test.js` +- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && node tests/unit/staff-family-asset-submit-flow.test.js` +- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && node tests/unit/staff-family-asset-maintenance-layout.test.js` +- 页面联调覆盖: + - 只导亲属关系 Sheet + - 只导亲属资产 Sheet + - 双 Sheet 同时导入 + - 两类失败记录列表都显示 `Sheet / Excel行号 / 失败原因` + +## 影响范围 +- `ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue` diff --git a/docs/plans/frontend/2026-04-23-bidding-supplier-validation-frontend-implementation.md b/docs/plans/frontend/2026-04-23-bidding-supplier-validation-frontend-implementation.md new file mode 100644 index 00000000..19e4c944 --- /dev/null +++ b/docs/plans/frontend/2026-04-23-bidding-supplier-validation-frontend-implementation.md @@ -0,0 +1,22 @@ +# 招投标供应商校验前端实施计划 + +## 目标 +- 将招投标信息维护页面新增、编辑弹窗中的供应商明细校验收敛为: + - 供应商名称必填 + - 统一信用代码必填 +- 移除联系人、联系电话、银行账户,以及供应商名称/统一信用代码的内容校验提示。 + +## 实施内容 +- 调整 `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue` 中的 `getSupplierFieldRules` + - `supplierName` 仅保留必填规则。 + - `supplierUscc` 改为仅保留必填规则。 + - `contactPerson`、`contactPhone`、`supplierBankAccount` 不再返回校验规则。 + +## 验证 +- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod` +- 使用 Playwright 打开真实页面 `http://127.0.0.1:62319/maintain/purchaseTransaction` + - 新增弹窗录入 `supplierUscc=ABC`、`contactPhone=123` 后可保存 + - 编辑弹窗录入 `supplierUscc=XYZ`、`contactPhone=abc123` 后可保存 + +## 产出文件 +- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue` diff --git a/docs/reports/implementation/2026-04-22-base-staff-dual-sheet-import-implementation.md b/docs/reports/implementation/2026-04-22-base-staff-dual-sheet-import-implementation.md new file mode 100644 index 00000000..fd18e056 --- /dev/null +++ b/docs/reports/implementation/2026-04-22-base-staff-dual-sheet-import-implementation.md @@ -0,0 +1,40 @@ +# 员工信息维护双 Sheet 导入实施记录 + +## 本次修改 +- 后端将员工信息维护导入模板改为双 Sheet: + - `员工信息` + - `员工资产信息` +- 后端导入入口统一到 `/ccdi/baseStaff/importData`,按有数据的 Sheet 分别提交员工导入任务与员工资产导入任务。 +- 员工信息导入移除了更新现有员工能力,现有员工 ID 或身份证号冲突统一进入失败记录。 +- 员工资产导入补充了重复校验,当前按 `personId + assetMainType + assetSubType + assetName` 识别重复,命中数据库或导入文件内重复时直接进入失败记录。 +- 前端删除独立“导入资产信息”按钮与独立资产上传弹窗,改为单入口上传双 Sheet 模板。 +- 前端上传成功后,按返回的两个任务 ID 分别沿用原有轮询、失败记录缓存和失败记录弹窗能力。 +- 修正员工导入失败记录字段为 `staffId`,保证失败记录列表能正确显示柜员号。 +- 员工失败记录与员工资产失败记录都增加了 `Sheet`、`Excel行号`、`失败原因` 定位信息。 + +## 影响范围 +- 后端 + - `ccdi-info-collection` 员工导入控制器、服务接口、异步导入服务、导入结果 VO +- 前端 + - `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` + - `ruoyi-ui/src/api/ccdiBaseStaff.js` + +## 验证情况 +- 后端编译 + - `mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile` + - 结果:通过 +- 前端构建 + - `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod` + - 结果:通过 + - 备注:构建输出仍有既有体积告警,但未阻断构建 + +## 测试说明 +- 已补充员工双 Sheet 导入相关单测文件,并执行: + - `mvn -pl ccdi-info-collection -am -Dtest=CcdiBaseStaffControllerTest,CcdiBaseStaffDualImportServiceTest,CcdiBaseStaffAssetImportServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test` + - 结果:通过 +- 已通过 Playwright 在真实员工信息维护页面验证: + - 页面仅保留一个“导入”按钮 + - 导入弹窗展示双 Sheet 提示文案 + - 两个失败入口可恢复显示 + - 员工失败弹窗显示 `Sheet / Excel行号 / 失败原因` + - 资产失败弹窗显示 `Sheet / Excel行号 / 失败原因` diff --git a/docs/reports/implementation/2026-04-22-base-staff-maintenance-browser-test-execution.md b/docs/reports/implementation/2026-04-22-base-staff-maintenance-browser-test-execution.md new file mode 100644 index 00000000..3813b432 --- /dev/null +++ b/docs/reports/implementation/2026-04-22-base-staff-maintenance-browser-test-execution.md @@ -0,0 +1,51 @@ +# 2026-04-22 员工信息维护真实页面测试执行记录 + +## 1. 执行内容 + +- 启动前端开发服务,端口使用 `1025` +- 使用真实浏览器进入“信息维护-员工信息维护”页面 +- 完整验证以下链路: + - 列表加载 + - 详情查看 + - 新增员工及资产 + - 编辑员工及资产 + - 删除 + - 导入模板下载 + - 双 Sheet 导入 + - 员工导入失败记录查看 + - 员工资产导入失败记录查看 + +## 2. 环境信息 + +- 前端地址:`http://127.0.0.1:1025` +- 后端地址:`http://127.0.0.1:62318` +- Mock 服务:`http://127.0.0.1:8000` +- Node 版本:`v14.21.3` +- 浏览器方式:Playwright headed 模式真实浏览器 + +## 3. 结果摘要 + +- 页面新增员工 `9260422` 成功,编辑成功,详情可正常展示资产明细。 +- 双 Sheet 导入成功写入员工 `9260423`,并成功向员工 `9260422` 导入资产 `导入资产车位A-0422`。 +- 员工导入失败记录正确命中 `该员工ID已存在`。 +- 员工资产导入失败记录正确命中 `员工资产导入仅支持员工本人证件号`。 +- 本轮新增与导入成功数据均已通过真实页面删除清理。 +- 页面本地导入缓存 `employee_import_last_task`、`employee_asset_import_last_task` 已清理。 + +## 4. 发现问题 + +- 员工详情弹窗中“所属部门”未正确回显。 +- 同一员工在列表中显示 `若依科技`,进入详情后该字段显示为 `-`。 + +## 5. 产出物 + +- 测试记录: + - `/Users/wkc/Desktop/ccdi/ccdi/docs/tests/records/2026-04-22-base-staff-maintenance-browser-test-record.md` +- 导入样本: + - `/Users/wkc/Desktop/ccdi/ccdi/output/spreadsheet/base_staff_import_browser_mixed.xlsx` + +## 6. 收尾情况 + +- 已关闭 Playwright 浏览器会话 +- 已关闭前端 `1025` 进程 +- 已停止本轮通过 `bin/restart_java_backend.sh` 拉起的后端进程 diff --git a/docs/reports/implementation/2026-04-22-bidding-import-failure-display-implementation.md b/docs/reports/implementation/2026-04-22-bidding-import-failure-display-implementation.md new file mode 100644 index 00000000..a3f47236 --- /dev/null +++ b/docs/reports/implementation/2026-04-22-bidding-import-failure-display-implementation.md @@ -0,0 +1,46 @@ +# 2026-04-22 招投标导入失败展示增强实施记录 + +## 1. 本次变更 + +- 招投标导入失败记录新增失败来源 `Sheet` +- 招投标导入失败记录新增 Excel 失败行号 +- 招投标信息维护页面失败弹窗新增 `失败Sheet`、`失败行号` 展示列 + +## 2. 文档落点确认 + +- 后端实施计划:`/Users/wkc/Desktop/ccdi/ccdi/docs/plans/backend/2026-04-22-bidding-import-failure-display-backend-implementation.md` +- 前端实施计划:`/Users/wkc/Desktop/ccdi/ccdi/docs/plans/frontend/2026-04-22-bidding-import-failure-display-frontend-implementation.md` +- 实施记录:`/Users/wkc/Desktop/ccdi/ccdi/docs/reports/implementation/2026-04-22-bidding-import-failure-display-implementation.md` + +## 3. 实施内容 + +- 在后端失败记录 VO 中新增 `sheetName`、`sheetRowNum` +- 在导入服务中为主信息、供应商明细两类导入行建立行号上下文 +- 在主信息校验失败、供应商校验失败、主从关系失败等场景下统一返回失败来源 Sheet 与行号 +- 在前端失败弹窗中新增 `失败Sheet`、`失败行号` 列,直接展示后端返回值 + +## 4. 预期验证点 + +- 主信息失败记录显示 `招投标主信息` +- 供应商失败记录显示 `供应商明细` +- 失败行号与 Excel 实际数据行一致 +- 同一失败由多行触发时,页面可直接展示合并行号 + +## 5. 实际验证结果 + +- 后端执行 `mvn -pl ccdi-info-collection -am compile -DskipTests` 编译通过 +- 后端通过 `bin/restart_java_backend.sh` 完成重启,前端通过 `nvm use` 后启动真实页面进行验证 +- 在真实页面上传 `bidding_import_structure.xlsx` 后,导入任务 `f1026563-4bf3-4f1d-ae27-d3f9623547f4` 成功生成失败记录 +- 页面“查看导入失败记录”弹窗已展示以下表头: +- `失败Sheet` +- `失败行号` +- `采购事项ID` +- `项目名称` +- `标的物名称` +- `失败原因` +- 真实页面已验证的失败展示样例: +- `供应商明细 | 第8行 | 供应商明细Sheet中的采购事项ID不能为空` +- `招投标主信息 | 第2行 | 采购事项ID[LSFXMOCKP2PUR001]已存在,请勿重复导入` +- `招投标主信息 | 第3、4行 | 采购事项ID[IMPCOV20260422154511STR03]在招投标主信息Sheet中重复` +- `供应商明细 | 第4、5行 | 采购事项ID[IMPCOV20260422154511STR02]缺少招投标主信息` +- 本次验证样本未产生成功导入数据,无需额外回滚业务数据 diff --git a/docs/reports/implementation/2026-04-22-staff-family-dual-sheet-import-implementation.md b/docs/reports/implementation/2026-04-22-staff-family-dual-sheet-import-implementation.md new file mode 100644 index 00000000..19783b11 --- /dev/null +++ b/docs/reports/implementation/2026-04-22-staff-family-dual-sheet-import-implementation.md @@ -0,0 +1,48 @@ +# 员工亲属关系维护双 Sheet 导入实施记录 + +## 本次修改 +- 将员工亲属关系维护顶部两个导入按钮合并为一个统一导入入口。 +- 导入模板改为双 Sheet: + - `员工亲属关系信息` + - `亲属资产信息` +- 导入提交入口统一到 `/ccdi/staffFmyRelation/importData`,按有数据的 Sheet 分别提交亲属关系任务和亲属资产任务。 +- 新增 `StaffFmyRelationImportSubmitResultVO`,返回 `relationTaskId`、`assetTaskId` 和提交提示文案。 +- 员工亲属关系失败记录与亲属资产失败记录都增加了 `Sheet`、`Excel行号`、`失败原因` 定位信息。 +- 前端删除独立亲属资产上传弹窗,但保留原有两套轮询和失败记录查看入口。 + +## 影响范围 +- 后端 + - `ccdi-info-collection` 员工亲属关系导入控制器、两类异步导入服务、失败记录 VO、导入提交结果 VO、相关控制器测试 +- 前端 + - `ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue` + - `ruoyi-ui/tests/unit/staff-family-asset-detail-import-ui.test.js` + +## 验证情况 +- 后端构建 + - `sh bin/restart_java_backend.sh restart` + - 结果:通过,`ruoyi-admin` 已基于本次代码重新打包并启动成功。 +- 前端单测 + - `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && node tests/unit/staff-family-asset-detail-import-ui.test.js` + - `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && node tests/unit/staff-family-asset-submit-flow.test.js` + - `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && node tests/unit/staff-family-asset-maintenance-layout.test.js` + - 结果:通过 +- 后端编译/测试 + - `mvn -pl ccdi-info-collection -am -Dtest=CcdiStaffFmyRelationControllerTest,CcdiAssetInfoControllerTest -Dsurefire.failIfNoSpecifiedTests=false test` + - 结果:通过,`Tests run: 10, Failures: 0, Errors: 0` +- 真实页面实测 + - 前端端口:`1026` + - 使用 Playwright 打开真实页面 `/maintain/staffFmyRelation` + - 实测结果: + - 页面顶部仅保留一个“导入”按钮 + - 从页面内下载到新的双 Sheet 模板,实际包含 `员工亲属关系信息`、`亲属资产信息` 两张表 + - 通过双 Sheet 失败样本上传后,页面同时出现“查看导入失败记录”和“查看亲属资产导入失败记录”两个失败入口 +- 后端接口核验 + - 使用双 Sheet 失败样本调用 `/ccdi/staffFmyRelation/importData` + - 再分别查询 `/ccdi/staffFmyRelation/importFailures/{relationTaskId}` 与 `/ccdi/assetInfo/importFailures/{assetTaskId}` + - 返回结果已确认包含: + - 亲属关系失败记录:`sheetName=员工亲属关系信息`、`rowNum=2`、`errorMessage=员工身份证号格式不正确` + - 亲属资产失败记录:`sheetName=亲属资产信息`、`rowNum=2`、`errorMessage=未找到亲属资产归属员工` + +## 后续实测 +- 本轮已完成真实页面双 Sheet 导入实测与失败接口核验。 +- 若后续需要补充界面截图或失败弹窗截图,可直接复用 `output/playwright/base-staff-maintenance-test/` 下本轮产物继续追踪。 diff --git a/docs/reports/implementation/2026-04-23-116-62-17-81-docker-deploy-record.md b/docs/reports/implementation/2026-04-23-116-62-17-81-docker-deploy-record.md new file mode 100644 index 00000000..dff3439b --- /dev/null +++ b/docs/reports/implementation/2026-04-23-116-62-17-81-docker-deploy-record.md @@ -0,0 +1,63 @@ +# 2026-04-23 `116.62.17.81:9444` Docker 部署记录 + +## 保存路径确认 + +- 目标目录:`docs/reports/implementation/` +- 文档用途:记录本次部署执行过程、影响范围与验证结果 +- 路径检查结果:符合仓库实施记录归档规范 + +## 本次操作 + +- 在本地仓库 `/Users/wkc/Desktop/ccdi/ccdi` 执行部署 +- 前端先通过 `nvm use` 切换到 `ruoyi-ui/.nvmrc` 指定版本:`Node v14.21.3` +- 执行后端打包:`mvn clean package -DskipTests` +- 执行前端打包:`cd ruoyi-ui && npm run build:prod` +- 执行部署脚本:`./deploy/deploy-to-nas.sh` +- 部署目标: + - SSH:`116.62.17.81:9444` + - 远端目录:`/volume1/webapp/ccdi` + - 宿主机内网地址:`192.168.0.111` + +## 影响范围 + +- 远端部署目录 `/volume1/webapp/ccdi` 已刷新为本次构建产物 +- Docker 服务已重建: + - `ccdi-backend` + - `ccdi-frontend` + - `ccdi-lsfx-mock` +- 本次仓库内仅新增本实施记录文档,无业务代码改动 + +## 验证结果 + +### 构建验证 + +- Maven 聚合打包成功,`ruoyi-admin/target/ruoyi-admin.jar` 已生成 +- Vue 生产构建成功,`ruoyi-ui/dist` 已生成 +- 前端构建期间仅出现体积告警,无构建失败 + +### 远端容器验证 + +- `docker compose ps` 结果: + - `ccdi-backend`:`Up` + - `ccdi-frontend`:`Up` + - `ccdi-lsfx-mock`:`Up` +- 端口监听结果: + - `62318 -> backend:8080` + - `62319 -> frontend:80` + - `62320 -> backend network / mock:8000` + +### 应用可用性验证 + +- 宿主机本机访问 `127.0.0.1` 返回正常: + - `http://127.0.0.1:62319` 返回 `200 OK` + - `http://127.0.0.1:62318/swagger-ui/index.html` 返回 `200` + - `http://127.0.0.1:62320/docs` 返回 `200 OK` +- 后端日志确认: + - `nas` profile 已启用 + - TongWeb `8080` 已启动 + - `RuoYiApplication` 启动完成 + +## 额外说明 + +- 通过公网地址 `116.62.17.81:62318/62319/62320` 访问时,当前观测到的是 `Empty reply from server` +- 由于宿主机实际网卡地址为 `192.168.0.111`,`116.62.17.81` 属于外层公网映射地址,因此当前应用侧与 Docker 侧均已正常,剩余问题若需继续处理,应排查公网入口到宿主机 `62318/62319/62320` 的端口转发或 NAT/网关链路 diff --git a/docs/reports/implementation/2026-04-23-base-staff-import-dept-validation-implementation.md b/docs/reports/implementation/2026-04-23-base-staff-import-dept-validation-implementation.md new file mode 100644 index 00000000..d9f70680 --- /dev/null +++ b/docs/reports/implementation/2026-04-23-base-staff-import-dept-validation-implementation.md @@ -0,0 +1,28 @@ +# 员工信息导入机构号校验实施记录 + +## 文档信息 +- 保存路径:`docs/reports/implementation/2026-04-23-base-staff-import-dept-validation-implementation.md` +- 实施日期:2026-04-23 +- 关联范围:员工信息维护后端导入链路 + +## 本次修改内容 +1. 在 `CcdiBaseStaffImportServiceImpl` 中新增 `deptId` 校验逻辑,要求导入员工的所属部门必须在 `sys_dept` 中存在且处于正常、未删除状态。 +2. 机构号校验失败时,导入记录不入库,失败原因统一写入现有失败记录字段 `errorMessage`,错误文案为 `所属部门ID[xxx]不存在或已停用/删除,请检查机构号`。 +3. 补齐 `SysDeptMapper.xml` 中 `selectDeptById` 的 `del_flag` 字段映射,避免导入服务误把逻辑删除部门识别为有效部门。 +4. 补充员工导入服务层与异步导入流程单元测试,覆盖部门不存在、部门停用、部门删除和合法/非法混合导入场景。 + +## 影响范围 +- 后端: + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java` + - `ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml` +- 测试: + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffImportServiceImplTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffDualImportServiceTest.java` + +## 验证情况 +1. 定向单测: + - 命令:`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiBaseStaffImportServiceImplTest,CcdiBaseStaffDualImportServiceTest test` + - 结果:通过,`Tests run: 13, Failures: 0, Errors: 0, Skipped: 0` +2. 编译校验: + - 命令:`mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile` + - 结果:通过,Reactor Summary 中 `ccdi-info-collection`、`ruoyi-admin` 及其依赖模块均为 `SUCCESS` diff --git a/docs/reports/implementation/2026-04-23-bidding-supplier-validation-implementation.md b/docs/reports/implementation/2026-04-23-bidding-supplier-validation-implementation.md new file mode 100644 index 00000000..28b5478c --- /dev/null +++ b/docs/reports/implementation/2026-04-23-bidding-supplier-validation-implementation.md @@ -0,0 +1,44 @@ +# 招投标供应商校验调整实施记录 + +## 本次改动 +- 招投标信息维护页面新增、编辑弹窗中的供应商明细校验调整为仅保留: + - 供应商名称必填 + - 统一信用代码必填 +- 移除了前端弹窗中供应商名称长度、统一信用代码格式、联系人长度、联系电话格式、银行账户长度校验。 +- 移除了新增/编辑接口 DTO 中对应的供应商内容校验,确保真实页面保存链路与弹窗规则一致。 + +## 关键文件 +- 前端 + - `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue` +- 后端 + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiPurchaseTransactionSupplierDTO.java` + +## 验证结果 +- 后端编译 + - 命令:`mvn -pl ccdi-info-collection -am -DskipTests compile` + - 结果:通过。 +- 后端重启 + - 命令:`sh bin/restart_java_backend.sh` + - 结果:构建并重启成功。 +- 前端构建 + - 命令:`source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod` + - 结果:通过,仅有既有产物体积 warning。 +- Playwright 实页验证 + - 页面:`http://127.0.0.1:62319/maintain/purchaseTransaction` + - 新增验证: + - 采购事项ID:`AUTOBID20260423145630` + - 供应商统一信用代码:`ABC` + - 供应商联系电话:`123` + - 结果:新增成功。 + - 编辑验证: + - 项目名称改为:`校验放开回归项目-145630-编辑` + - 供应商统一信用代码改为:`XYZ` + - 供应商联系电话改为:`abc123` + - 结果:修改成功。 + - 清理验证: + - 删除 `AUTOBID20260423145630` + - 结果:删除成功,列表总数回到 `2004`。 + +## 过程说明 +- 首轮真实页验证发现,前端规则放开后,新增接口仍因 `CcdiPurchaseTransactionSupplierDTO` 的 Bean Validation 拦截 `supplierUscc` 和 `contactPhone`。 +- 因此本次最终方案同时调整了前端弹窗规则与后端 DTO 校验,保证页面行为与需求一致。 diff --git a/docs/reports/implementation/2026-04-23-info-maintenance-search-grid-implementation.md b/docs/reports/implementation/2026-04-23-info-maintenance-search-grid-implementation.md new file mode 100644 index 00000000..2cfcad47 --- /dev/null +++ b/docs/reports/implementation/2026-04-23-info-maintenance-search-grid-implementation.md @@ -0,0 +1,188 @@ +# 信息维护页面搜索区四列栅格统一实施记录 + +## 1. 实施目标 + +将信息维护同批页面的搜索区统一为每行 4 个字段位的栅格布局,超过 4 个字段自动换行,最后一行不足 4 项时保留空白字段位,不拉伸现有字段;同时清理查询区内遗留的固定像素宽度写法。 + +## 2. 实施范围 + +本次实际调整的前端文件如下: + +- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` +- `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue` +- `ruoyi-ui/src/views/ccdiStaffTransfer/index.vue` +- `ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue` +- `ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue` +- `ruoyi-ui/src/views/ccdiCreditInfo/index.vue` +- `ruoyi-ui/src/views/ccdiEnterpriseBaseInfo/index.vue` +- `ruoyi-ui/src/views/ccdiIntermediary/components/SearchForm.vue` +- `ruoyi-ui/src/views/ccdiAccountInfo/index.vue` +- `ruoyi-ui/src/views/ccdiCustFmyRelation/index.vue` +- `ruoyi-ui/src/views/ccdiCustEnterpriseRelation/index.vue` +- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue` + +本轮未提交的临时验证文件: + +- `output/playwright/info-maintenance-search-grid-check.js` + +## 3. 实施内容 + +### 3.1 查询区统一为四列栅格 + +- 将目标页面的头部查询表单从 `:inline="true"` 流式排布改为 `el-row + el-col :span="6"` 栅格排布 +- 每个查询项固定占一个字段位 +- 超过 4 个字段自动换到下一行 +- 最后一行不足 4 项时保留空白字段位,不做横向拉伸 + +### 3.2 字段内部控件统一铺满字段位 + +- 将查询区里的输入框、下拉框、日期范围、树选择统一改为 `style="width: 100%"` +- 清理查询区中遗留的固定像素宽度写法,不再依赖 `style="width: Npx"` 控制布局 +- 补充查询区专用样式,使 `el-row` 使用换行布局,`el-col` 不再依赖旧浮动行为导致错位 + +### 3.3 保持原有交互和业务逻辑不变 + +- 未修改 `queryParams`、`handleQuery`、`resetQuery` +- 未修改搜索、重置、业务按钮、`right-toolbar` 的行为 +- 日期范围仍按一个字段位处理,仅在所属字段位内铺满 + +## 4. TDD / 结构校验 + +### 4.1 先写临时结构校验脚本 + +新增临时脚本: + +- `output/playwright/info-maintenance-search-grid-check.js` + +脚本校验口径: + +- 只识别头部查询表单,不误扫弹窗表单 +- 查询区是否采用 `el-col :span="6"` +- 查询项数量是否符合预期 +- 查询区内是否仍保留 `style="width: Npx"` 固定像素宽度 +- 账户库管理页是否仍残留旧的查询区局部覆盖样式 + +### 4.2 先失败,再通过 + +首次执行命令: + +```bash +source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use >/dev/null && cd .. && node output/playwright/info-maintenance-search-grid-check.js +``` + +初次结果: + +- 12 个目标页面全部报出“未使用 `span=6`”和“仍存在固定像素宽度”的失败 + +页面改造完成后再次执行同一命令,结果为: + +- `信息维护搜索区四列栅格结构校验通过` + +## 5. 构建验证 + +### 5.1 Node 版本 + +执行命令: + +```bash +source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use +``` + +结果: + +- 命中项目 `nvm` 配置 +- 使用项目要求的 Node 版本完成后续脚本和构建 + +### 5.2 前端构建 + +执行命令: + +```bash +source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use >/dev/null && npm run build:prod +``` + +结果: + +- 构建成功 +- 无新增模板语法错误 +- 仅保留项目原有的体积告警 + +## 6. 真实页面浏览器验证 + +### 6.1 启动服务 + +前端开发服务: + +```bash +source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use >/dev/null && npm_config_port=1025 npm run dev +``` + +后端服务: + +```bash +sh bin/restart_java_backend.sh start +``` + +说明: + +- 本轮验证时后端原本未运行,因此使用仓库规定脚本启动 +- 使用真实登录页进入系统,账号为 `admin / admin123` + +### 6.2 布局验证结果 + +使用 Playwright 进入以下代表页并读取查询区实际列位位置: + +- `/maintain/baseStaff` +- `/maintain/enterpriseBaseInfo` +- `/maintain/accountInfo` +- `/maintain/creditInfo` +- `/maintain/intermediary` +- `/maintain/staffTransfer` +- `/maintain/purchaseTransaction` + +验证结果: + +- 员工信息维护:5 个字段,实际排布为 `4 + 1` +- 实体库管理:8 个字段,实际排布为 `4 + 4` +- 账户库管理:9 个字段,实际排布为 `4 + 4 + 1` +- 征信维护:2 个字段,实际排布为单行 2 项,未拉伸成半屏布局 +- 中介库管理:4 个字段,实际排布为单行 4 列 +- 员工调动记录:6 个字段,实际排布为 `4 + 2` +- 招投标信息维护:4 个字段,日期范围与其他查询项等宽,占 1 个字段位 + +浏览器读取到的列位位置表现一致: + +- 同一行 4 列的左边界稳定为四等分位置 +- 超过 4 个字段的新一行从第一列起始位置重新排列 +- 最后一行不足 4 项时未发生拉伸 + +### 6.3 重置验证 + +在真实业务页面中使用 Playwright 做了两组输入与重置验证: + +1. 员工信息维护 + - 向“姓名”输入框填入 `测试姓名` + - 点击页面 `重置` + - 再次读取输入框值,结果为空字符串 + +2. 账户库管理 + - 向“员工姓名”输入框填入 `验证员工` + - 点击页面 `重置` + - 再次读取输入框值,结果为空字符串 + +结论: + +- 搜索区改为四列栅格后,`重置` 交互未受影响 + +## 7. 进程清理 + +本轮验证结束后已关闭: + +- 前端开发服务 `npm run dev` +- 后端服务 `sh bin/restart_java_backend.sh start` 启动的后端进程,使用 `sh bin/restart_java_backend.sh stop` 停止 +- Playwright 浏览器会话 + +## 8. 风险与说明 + +- 当前工作区存在其他与本任务无关的未提交改动,本次未回退这些既有改动,仅在搜索区四列栅格范围内继续修改目标页面 +- 临时结构校验脚本位于 `output/playwright/`,用于本轮 TDD 校验,不纳入 git 提交 diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml index cf439f68..eff8d492 100644 --- a/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -59,7 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"