diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiBaseStaffControllerTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiBaseStaffControllerTest.java new file mode 100644 index 00000000..dd118fd4 --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiBaseStaffControllerTest.java @@ -0,0 +1,147 @@ +package com.ruoyi.info.collection.controller; + +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel; +import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel; +import com.ruoyi.info.collection.domain.vo.BaseStaffImportSubmitResultVO; +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; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiBaseStaffControllerTest { + + @InjectMocks + private CcdiBaseStaffController controller; + + @Mock + private ICcdiBaseStaffService baseStaffService; + + @Mock + private ICcdiBaseStaffImportService importAsyncService; + + @Mock + private ICcdiBaseStaffAssetImportService baseStaffAssetImportService; + + @Test + void importTemplate_shouldDownloadDualSheetTemplate() { + try (MockedStatic mocked = mockStatic(EasyExcelUtil.class)) { + controller.importTemplate(null); + + mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown( + null, + CcdiBaseStaffExcel.class, + "员工信息", + CcdiBaseStaffAssetInfoExcel.class, + "员工资产信息", + "员工信息维护导入模板" + )); + } + } + + @Test + void importData_shouldWarnWhenBothSheetsAreEmpty() throws Exception { + MockMultipartFile file = new MockMultipartFile( + "file", + "base-staff-empty.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "empty".getBytes(StandardCharsets.UTF_8) + ); + + try (MockedStatic mocked = mockStatic(EasyExcelUtil.class)) { + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息"))) + .thenReturn(List.of()); + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息"))) + .thenReturn(List.of()); + + AjaxResult result = controller.importData(file); + + assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG)); + assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG)); + verifyNoInteractions(baseStaffService); + } + } + + @Test + void importData_shouldSubmitOnlyStaffTaskWhenOnlyStaffSheetHasRows() throws Exception { + MockMultipartFile file = new MockMultipartFile( + "file", + "base-staff.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "staff".getBytes(StandardCharsets.UTF_8) + ); + CcdiBaseStaffExcel staffExcel = new CcdiBaseStaffExcel(); + staffExcel.setStaffId(1001L); + BaseStaffImportSubmitResultVO submitResult = new BaseStaffImportSubmitResultVO(); + submitResult.setStaffTaskId("staff-task-1"); + when(baseStaffService.importBaseStaffWithAssets(List.of(staffExcel), List.of())).thenReturn(submitResult); + + try (MockedStatic mocked = mockStatic(EasyExcelUtil.class)) { + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息"))) + .thenReturn(List.of(staffExcel)); + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息"))) + .thenReturn(List.of()); + + AjaxResult result = controller.importData(file); + + assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG)); + assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG)); + Object data = result.get(AjaxResult.DATA_TAG); + assertEquals("staff-task-1", data.getClass().getMethod("getStaffTaskId").invoke(data)); + assertNull(data.getClass().getMethod("getAssetTaskId").invoke(data)); + } + } + + @Test + void importData_shouldSubmitTwoTasksWhenBothSheetsHaveRows() throws Exception { + MockMultipartFile file = new MockMultipartFile( + "file", + "base-staff-both.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "both".getBytes(StandardCharsets.UTF_8) + ); + CcdiBaseStaffExcel staffExcel = new CcdiBaseStaffExcel(); + staffExcel.setStaffId(1002L); + CcdiBaseStaffAssetInfoExcel assetExcel = new CcdiBaseStaffAssetInfoExcel(); + assetExcel.setPersonId("320101199001010011"); + BaseStaffImportSubmitResultVO submitResult = new BaseStaffImportSubmitResultVO(); + submitResult.setStaffTaskId("staff-task-2"); + submitResult.setAssetTaskId("asset-task-2"); + when(baseStaffService.importBaseStaffWithAssets(List.of(staffExcel), List.of(assetExcel))).thenReturn(submitResult); + + try (MockedStatic mocked = mockStatic(EasyExcelUtil.class)) { + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息"))) + .thenReturn(List.of(staffExcel)); + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息"))) + .thenReturn(List.of(assetExcel)); + + AjaxResult result = controller.importData(file); + + assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG)); + Object data = result.get(AjaxResult.DATA_TAG); + assertEquals("staff-task-2", data.getClass().getMethod("getStaffTaskId").invoke(data)); + assertEquals("asset-task-2", data.getClass().getMethod("getAssetTaskId").invoke(data)); + } + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiEnumControllerTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiEnumControllerTest.java new file mode 100644 index 00000000..e58df86c --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiEnumControllerTest.java @@ -0,0 +1,37 @@ +package com.ruoyi.info.collection.controller; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.info.collection.domain.vo.EnumOptionVO; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class CcdiEnumControllerTest { + + private final CcdiEnumController controller = new CcdiEnumController(); + + @Test + void getEnterpriseRiskLevelOptions_shouldReturnConfiguredOptions() { + AjaxResult result = controller.getEnterpriseRiskLevelOptions(); + List data = (List) result.get("data"); + + assertEquals(3, data.size()); + EnumOptionVO first = (EnumOptionVO) data.get(0); + assertEquals("1", first.getValue()); + assertEquals("高风险", first.getLabel()); + } + + @Test + void getEnterpriseSourceOptions_shouldReturnConfiguredOptions() { + AjaxResult result = controller.getEnterpriseSourceOptions(); + List data = (List) result.get("data"); + + assertFalse(data.isEmpty()); + EnumOptionVO first = (EnumOptionVO) data.get(0); + assertEquals("GENERAL", first.getValue()); + assertEquals("一般企业", first.getLabel()); + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationControllerTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationControllerTest.java new file mode 100644 index 00000000..d6c4534a --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationControllerTest.java @@ -0,0 +1,173 @@ +package com.ruoyi.info.collection.controller; + +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel; +import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel; +import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO; +import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO; +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; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiStaffFmyRelationControllerTest { + + @InjectMocks + private CcdiStaffFmyRelationController controller; + + @Mock + private ICcdiStaffFmyRelationService relationService; + + @Mock + private ICcdiStaffFmyRelationImportService relationImportService; + + @Mock + private ICcdiAssetInfoImportService assetInfoImportService; + + @Test + void importTemplate_shouldDownloadDualSheetTemplate() { + try (MockedStatic mocked = mockStatic(EasyExcelUtil.class)) { + controller.importTemplate(null); + + mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown( + null, + CcdiStaffFmyRelationExcel.class, + "员工亲属关系信息", + CcdiAssetInfoExcel.class, + "亲属资产信息", + "员工亲属关系维护导入模板" + )); + } + } + + @Test + void importData_shouldErrorWhenBothSheetsAreEmpty() throws Exception { + MockMultipartFile file = new MockMultipartFile( + "file", + "staff-family-empty.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "empty".getBytes(StandardCharsets.UTF_8) + ); + + try (MockedStatic mocked = mockStatic(EasyExcelUtil.class)) { + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息"))) + .thenReturn(List.of()); + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息"))) + .thenReturn(List.of()); + + AjaxResult result = controller.importData(file); + + assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG)); + assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG)); + verifyNoInteractions(relationService, assetInfoImportService); + } + } + + @Test + void importData_shouldSubmitOnlyRelationTaskWhenOnlyRelationSheetHasRows() throws Exception { + MockMultipartFile file = new MockMultipartFile( + "file", + "staff-family-relation.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "relation".getBytes(StandardCharsets.UTF_8) + ); + CcdiStaffFmyRelationExcel relationExcel = new CcdiStaffFmyRelationExcel(); + relationExcel.setPersonId("320101199001010011"); + StaffFmyRelationImportSubmitResultVO submitResult = new StaffFmyRelationImportSubmitResultVO(); + submitResult.setRelationTaskId("relation-task-1"); + when(relationService.importRelationWithAssets(List.of(relationExcel), List.of())).thenReturn(submitResult); + + try (MockedStatic mocked = mockStatic(EasyExcelUtil.class)) { + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息"))) + .thenReturn(List.of(relationExcel)); + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息"))) + .thenReturn(List.of()); + + AjaxResult result = controller.importData(file); + + assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG)); + assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG)); + Object data = result.get(AjaxResult.DATA_TAG); + assertEquals("relation-task-1", data.getClass().getMethod("getRelationTaskId").invoke(data)); + assertNull(data.getClass().getMethod("getAssetTaskId").invoke(data)); + } + } + + @Test + void importData_shouldSubmitTwoTasksWhenBothSheetsHaveRows() throws Exception { + MockMultipartFile file = new MockMultipartFile( + "file", + "staff-family-both.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "both".getBytes(StandardCharsets.UTF_8) + ); + CcdiStaffFmyRelationExcel relationExcel = new CcdiStaffFmyRelationExcel(); + relationExcel.setPersonId("320101199001010012"); + CcdiAssetInfoExcel assetExcel = new CcdiAssetInfoExcel(); + assetExcel.setPersonId("320101199001010099"); + StaffFmyRelationImportSubmitResultVO submitResult = new StaffFmyRelationImportSubmitResultVO(); + submitResult.setRelationTaskId("relation-task-2"); + submitResult.setAssetTaskId("asset-task-2"); + when(relationService.importRelationWithAssets(List.of(relationExcel), List.of(assetExcel))).thenReturn(submitResult); + + try (MockedStatic mocked = mockStatic(EasyExcelUtil.class)) { + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息"))) + .thenReturn(List.of(relationExcel)); + mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息"))) + .thenReturn(List.of(assetExcel)); + + AjaxResult result = controller.importData(file); + + assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG)); + Object data = result.get(AjaxResult.DATA_TAG); + assertEquals("relation-task-2", data.getClass().getMethod("getRelationTaskId").invoke(data)); + assertEquals("asset-task-2", data.getClass().getMethod("getAssetTaskId").invoke(data)); + } + } + + @Test + void getImportFailures_shouldReturnPagedRowsWithSheetAndRowInfo() { + StaffFmyRelationImportFailureVO failure1 = new StaffFmyRelationImportFailureVO(); + failure1.setSheetName("员工亲属关系信息"); + failure1.setRowNum(2); + failure1.setPersonId("A1"); + + StaffFmyRelationImportFailureVO failure2 = new StaffFmyRelationImportFailureVO(); + failure2.setSheetName("员工亲属关系信息"); + failure2.setRowNum(3); + failure2.setPersonId("A2"); + + when(relationImportService.getImportFailures("task-1")).thenReturn(List.of(failure1, failure2)); + + TableDataInfo result = controller.getImportFailures("task-1", 2, 1); + + assertEquals(2, result.getTotal()); + assertEquals(1, result.getRows().size()); + StaffFmyRelationImportFailureVO row = (StaffFmyRelationImportFailureVO) result.getRows().get(0); + assertEquals("员工亲属关系信息", row.getSheetName()); + assertEquals(3, row.getRowNum()); + assertEquals("A2", row.getPersonId()); + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiEnterpriseBaseInfoMapperTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiEnterpriseBaseInfoMapperTest.java new file mode 100644 index 00000000..af5c6c1e --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiEnterpriseBaseInfoMapperTest.java @@ -0,0 +1,25 @@ +package com.ruoyi.info.collection.mapper; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiEnterpriseBaseInfoMapperTest { + + @Test + void mapperXml_shouldContainPageQueryAndImportColumns() throws Exception { + try (InputStream inputStream = getClass().getClassLoader() + .getResourceAsStream("mapper/info/collection/CcdiEnterpriseBaseInfoMapper.xml")) { + String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + + assertTrue(xml.contains("selectEnterpriseBaseInfoPage"), xml); + assertTrue(xml.contains("risk_level"), xml); + assertTrue(xml.contains("ent_source"), xml); + assertTrue(xml.contains("data_source"), xml); + assertTrue(xml.contains("ORDER BY create_time DESC"), xml); + } + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java new file mode 100644 index 00000000..87c1e62b --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java @@ -0,0 +1,27 @@ +package com.ruoyi.info.collection.mapper; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiIntermediaryMapperTest { + + @Test + void mapperXml_shouldContainThreeRecordTypesAndRelatedKeywordQuery() throws Exception { + try (InputStream inputStream = getClass().getClassLoader() + .getResourceAsStream("mapper/info/collection/CcdiIntermediaryMapper.xml")) { + String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + + assertTrue(xml.contains("INTERMEDIARY"), xml); + assertTrue(xml.contains("RELATIVE"), xml); + assertTrue(xml.contains("ENTERPRISE_RELATION"), xml); + assertTrue(xml.contains("relatedIntermediaryKeyword"), xml); + assertTrue(xml.contains("related_intermediary_name"), xml); + assertTrue(xml.contains("relation_text"), xml); + assertTrue(xml.contains("CAST('实体'"), xml); + } + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffDualImportServiceTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffDualImportServiceTest.java new file mode 100644 index 00000000..48df9b41 --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffDualImportServiceTest.java @@ -0,0 +1,163 @@ +package com.ruoyi.info.collection.service; + +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.info.collection.domain.CcdiBaseStaff; +import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel; +import com.ruoyi.info.collection.domain.vo.ImportFailureVO; +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.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiBaseStaffDualImportServiceTest { + + @InjectMocks + private CcdiBaseStaffImportServiceImpl service; + + @Mock + private com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper baseStaffMapper; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private HashOperations hashOperations; + + @Mock + private ValueOperations valueOperations; + + @Mock + private SysDeptMapper deptMapper; + + @Test + void importBaseStaffAsync_shouldTreatExistingEmployeeAsFailureInsteadOfUpdate() { + CcdiBaseStaffExcel excel = new CcdiBaseStaffExcel(); + excel.setStaffId(1001L); + excel.setName("张三"); + excel.setDeptId(10L); + excel.setIdCard("11010519491231002X"); + excel.setPhone("13812345678"); + excel.setStatus("0"); + excel.setPartyMember(1); + + CcdiBaseStaff existing = new CcdiBaseStaff(); + existing.setStaffId(1001L); + existing.setIdCard("11010519491231002X"); + + when(baseStaffMapper.selectBatchIds(List.of(1001L))).thenReturn(List.of(existing)); + when(baseStaffMapper.selectList(any())).thenReturn(List.of(existing)); + lenient().when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0")); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + + service.importBaseStaffAsync(List.of(excel), "task-existing"); + + verify(baseStaffMapper, never()).insertBatch(any()); + verify(baseStaffMapper, never()).insertOrUpdateBatch(any()); + + ArgumentCaptor failureCaptor = ArgumentCaptor.forClass(Object.class); + verify(valueOperations).set(eq("import:baseStaff:task-existing:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS)); + ImportFailureVO failure = (ImportFailureVO) ((List) failureCaptor.getValue()).get(0); + assertEquals("员工信息", failure.getSheetName()); + assertEquals(2, failure.getRowNum()); + assertEquals(1001L, failure.getStaffId()); + assertEquals("该员工ID已存在", failure.getErrorMessage()); + } + + @Test + void validateStaffData_shouldRejectExistingIdCardWhenStaffIdDoesNotExist() { + when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0")); + + RuntimeException exception = org.junit.jupiter.api.Assertions.assertThrows( + RuntimeException.class, + () -> service.validateStaffData(buildExcelDto(), Set.of(), Set.of("11010519491231002X")) + ); + + assertEquals("该身份证号已存在", exception.getMessage()); + } + + @Test + void importBaseStaffAsync_shouldSaveFailureWhenDeptIsInvalid() { + CcdiBaseStaffExcel validExcel = buildExcel(1001L, 10L, "11010519491231002X"); + CcdiBaseStaffExcel invalidExcel = buildExcel(1002L, 99L, "320101199001010014"); + + when(baseStaffMapper.selectBatchIds(List.of(1001L, 1002L))).thenReturn(List.of()); + when(baseStaffMapper.selectList(any())).thenReturn(List.of()); + when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0")); + when(deptMapper.selectDeptById(99L)).thenReturn(null); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + + service.importBaseStaffAsync(List.of(validExcel, invalidExcel), "task-invalid-dept"); + + verify(baseStaffMapper).insertBatch(any()); + + ArgumentCaptor failureCaptor = ArgumentCaptor.forClass(Object.class); + verify(valueOperations).set(eq("import:baseStaff:task-invalid-dept:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS)); + ImportFailureVO failure = (ImportFailureVO) ((List) failureCaptor.getValue()).get(0); + assertEquals("员工信息", failure.getSheetName()); + assertEquals(3, failure.getRowNum()); + assertEquals(1002L, failure.getStaffId()); + assertEquals("所属部门ID[99]不存在或已停用/删除,请检查机构号", failure.getErrorMessage()); + + ArgumentCaptor> statusCaptor = ArgumentCaptor.forClass(Map.class); + verify(hashOperations).putAll(eq("import:baseStaff:task-invalid-dept"), statusCaptor.capture()); + assertEquals("PARTIAL_SUCCESS", statusCaptor.getValue().get("status")); + assertEquals(1, statusCaptor.getValue().get("successCount")); + assertEquals(1, statusCaptor.getValue().get("failureCount")); + } + + private com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO buildExcelDto() { + com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO dto = new com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO(); + dto.setName("李四"); + dto.setStaffId(2001L); + dto.setDeptId(10L); + dto.setIdCard("11010519491231002X"); + dto.setPhone("13812345678"); + dto.setStatus("0"); + dto.setPartyMember(1); + return dto; + } + + private CcdiBaseStaffExcel buildExcel(Long staffId, Long deptId, String idCard) { + CcdiBaseStaffExcel excel = new CcdiBaseStaffExcel(); + excel.setStaffId(staffId); + excel.setName("张三"); + excel.setDeptId(deptId); + excel.setIdCard(idCard); + excel.setPhone("13812345678"); + excel.setStatus("0"); + excel.setPartyMember(1); + return excel; + } + + private SysDept buildDept(Long deptId, String status, String delFlag) { + SysDept dept = new SysDept(); + dept.setDeptId(deptId); + dept.setDeptName("测试部门"); + dept.setStatus(status); + dept.setDelFlag(delFlag); + return dept; + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java new file mode 100644 index 00000000..8a6d3643 --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java @@ -0,0 +1,113 @@ +package com.ruoyi.info.collection.service; + +import com.ruoyi.info.collection.domain.CcdiBizIntermediary; +import com.ruoyi.info.collection.domain.dto.CcdiIntermediaryRelativeAddDTO; +import com.ruoyi.info.collection.domain.dto.CcdiIntermediaryPersonAddDTO; +import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper; +import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper; +import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper; +import com.ruoyi.info.collection.mapper.CcdiIntermediaryMapper; +import com.ruoyi.info.collection.service.impl.CcdiIntermediaryServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiIntermediaryServiceImplTest { + + @InjectMocks + private CcdiIntermediaryServiceImpl service; + + @Mock + private CcdiBizIntermediaryMapper bizIntermediaryMapper; + + @Mock + private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper; + + @Mock + private CcdiIntermediaryMapper intermediaryMapper; + + @Mock + private CcdiIntermediaryEnterpriseRelationMapper enterpriseRelationMapper; + + @Mock + private ICcdiIntermediaryPersonImportService personImportService; + + @Mock + private ICcdiIntermediaryEntityImportService entityImportService; + + @Mock + private RedisTemplate redisTemplate; + + @Test + void insertIntermediaryPerson_shouldForceBenrenAndClearRelatedNumId() { + CcdiIntermediaryPersonAddDTO addDTO = new CcdiIntermediaryPersonAddDTO(); + addDTO.setName("测试中介"); + addDTO.setPersonId("320101199001010011"); + addDTO.setPersonSubType("配偶"); + addDTO.setRelatedNumId("parent-id"); + + when(bizIntermediaryMapper.selectCount(any())).thenReturn(0L); + when(bizIntermediaryMapper.insert(any(CcdiBizIntermediary.class))).thenReturn(1); + + int result = service.insertIntermediaryPerson(addDTO); + + assertEquals(1, result); + ArgumentCaptor captor = ArgumentCaptor.forClass(CcdiBizIntermediary.class); + verify(bizIntermediaryMapper).insert(captor.capture()); + assertEquals("本人", captor.getValue().getPersonSubType()); + assertNull(captor.getValue().getRelatedNumId()); + assertEquals("MANUAL", captor.getValue().getDataSource()); + } + + @Test + void insertIntermediaryRelative_shouldRejectBenrenSubType() { + CcdiBizIntermediary owner = new CcdiBizIntermediary(); + owner.setBizId("biz-1"); + owner.setPersonSubType("本人"); + + CcdiIntermediaryRelativeAddDTO addDTO = new CcdiIntermediaryRelativeAddDTO(); + addDTO.setName("测试亲属"); + addDTO.setPersonId("320101199001010022"); + addDTO.setPersonSubType("本人"); + + when(bizIntermediaryMapper.selectById("biz-1")).thenReturn(owner); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> service.insertIntermediaryRelative("biz-1", addDTO)); + + assertEquals("亲属关系不能为本人", exception.getMessage()); + verify(bizIntermediaryMapper, never()).insert(any(CcdiBizIntermediary.class)); + } + + @Test + void deleteIntermediaryByIds_shouldDeleteRelativesAndEnterpriseRelationsWhenRemovingOwner() { + CcdiBizIntermediary owner = new CcdiBizIntermediary(); + owner.setBizId("biz-1"); + owner.setPersonSubType("本人"); + + when(bizIntermediaryMapper.selectById("biz-1")).thenReturn(owner); + when(bizIntermediaryMapper.delete(any())).thenReturn(2); + when(enterpriseRelationMapper.delete(any())).thenReturn(1); + when(bizIntermediaryMapper.deleteById("biz-1")).thenReturn(1); + + int result = service.deleteIntermediaryByIds(new String[]{"biz-1"}); + + assertEquals(1, result); + verify(bizIntermediaryMapper).delete(any()); + verify(enterpriseRelationMapper).delete(any()); + verify(bizIntermediaryMapper).deleteById("biz-1"); + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiPurchaseTransactionFeatureContractTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiPurchaseTransactionFeatureContractTest.java new file mode 100644 index 00000000..725a7564 --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiPurchaseTransactionFeatureContractTest.java @@ -0,0 +1,78 @@ +package com.ruoyi.info.collection.service; + +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiPurchaseTransactionFeatureContractTest { + + @Test + void shouldExposeSupplierListContractsAcrossPurchaseTransactionModels() throws Exception { + assertHasField( + "com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO", + "supplierList" + ); + assertHasField( + "com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO", + "supplierList" + ); + assertHasField( + "com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO", + "supplierList" + ); + assertHasField( + "com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO", + "supplierCount" + ); + assertNotNull(Class.forName("com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier")); + assertNotNull(Class.forName("com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionSupplierDTO")); + assertNotNull(Class.forName("com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionSupplierVO")); + assertNotNull(Class.forName("com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel")); + } + + @Test + void shouldDefineSupplierSubTableAndBiddingMigrationScripts() throws Exception { + String initSql = Files.readString(repoPath("sql/ccdi_purchase_transaction.sql")); + assertTrue(initSql.contains("CREATE TABLE `ccdi_purchase_transaction_supplier`")); + assertTrue(initSql.contains("`is_bid_winner`")); + assertTrue(initSql.contains("`sort_order`")); + assertTrue(initSql.contains("utf8mb4_general_ci")); + + String menuSql = Files.readString(repoPath("sql/ccdi_purchase_transaction_menu.sql")); + assertTrue(menuSql.contains("招投标信息维护")); + + Path migrationPath = repoPath("sql/migration/2026-04-22-bidding-info-maintenance-supplier-detail.sql"); + assertTrue(Files.exists(migrationPath), "应提供招投标供应商明细迁移脚本"); + + String migrationSql = Files.readString(migrationPath); + assertTrue(migrationSql.contains("ccdi_purchase_transaction_supplier")); + assertTrue(migrationSql.contains("INSERT INTO ccdi_purchase_transaction_supplier")); + assertTrue(migrationSql.contains("UPDATE sys_menu")); + assertTrue(migrationSql.contains("招投标信息维护")); + } + + @Test + void shouldUseTwoSheetTemplateForBiddingImport() throws Exception { + assertNotNull(Class.forName("com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel")); + + String controller = Files.readString( + Path.of("src/main/java/com/ruoyi/info/collection/controller/CcdiPurchaseTransactionController.java") + ); + assertTrue(controller.contains("招投标主信息")); + assertTrue(controller.contains("供应商明细")); + } + + private void assertHasField(String className, String fieldName) throws Exception { + Class clazz = Class.forName(className); + Field field = clazz.getDeclaredField(fieldName); + assertNotNull(field); + } + + private Path repoPath(String relativePath) { + return Path.of("..", relativePath); + } +} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java new file mode 100644 index 00000000..86dfa244 --- /dev/null +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java @@ -0,0 +1,248 @@ +package com.ruoyi.info.collection.utils; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel; +import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class EasyExcelUtilImportDropdownValidationTest { + + @Test + void importExcel_shouldPassWhenAllDictDropdownColumnsKeepListValidation() throws Exception { + byte[] bytes = baseStaffWorkbook(true, true, true, 2); + + List rows = EasyExcelUtil.importExcel( + new ByteArrayInputStream(bytes), + CcdiBaseStaffExcel.class, + "员工信息" + ); + + assertEquals(2, rows.size()); + } + + @Test + void importExcel_shouldFailWhenPartyMemberDropdownIsMissing() throws Exception { + byte[] bytes = baseStaffWorkbook(false, true, true, 2); + + ServiceException exception = assertThrows(ServiceException.class, () -> + EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息") + ); + + assertTrue(exception.getMessage().contains("是否党员 列缺少下拉框")); + } + + @Test + void importExcel_shouldFailWhenStatusDropdownIsMissing() throws Exception { + byte[] bytes = baseStaffWorkbook(true, false, true, 2); + + ServiceException exception = assertThrows(ServiceException.class, () -> + EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息") + ); + + assertEquals("员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage()); + } + + @Test + void importExcel_shouldReportAllMissingDropdownColumnsInSameSheet() throws Exception { + byte[] bytes = baseStaffWorkbook(false, false, true, 2); + + ServiceException exception = assertThrows(ServiceException.class, () -> + EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息") + ); + + assertEquals("员工信息 Sheet 的 是否党员、状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage()); + } + + @Test + void importExcel_shouldFailWhenValidationIsNotListType() throws Exception { + byte[] bytes = baseStaffWorkbook(true, true, false, 2); + + ServiceException exception = assertThrows(ServiceException.class, () -> + EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息") + ); + + assertTrue(exception.getMessage().contains("状态 列缺少下拉框")); + } + + @Test + void importExcel_shouldFailWhenListValidationDoesNotCoverEveryActualDataRow() throws Exception { + byte[] bytes = baseStaffWorkbook(true, true, true, 1); + + ServiceException exception = assertThrows(ServiceException.class, () -> + EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息") + ); + + assertTrue(exception.getMessage().contains("状态 列缺少下拉框")); + } + + @Test + void importExcel_shouldFailWhenSecondSheetDropdownIsMissing() throws Exception { + byte[] bytes = baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown(); + + ServiceException exception = assertThrows(ServiceException.class, () -> + EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffAssetInfoExcel.class, "员工资产信息") + ); + + assertEquals("员工资产信息 Sheet 的 资产状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage()); + } + + @Test + void importExcel_shouldSkipDropdownStructureValidationWhenClassHasNoDictDropdownFields() throws Exception { + byte[] bytes = plainWorkbookWithoutDropdown(); + + List rows = EasyExcelUtil.importExcel( + new ByteArrayInputStream(bytes), + PlainExcel.class, + "普通信息" + ); + + assertEquals(1, rows.size()); + } + + private byte[] baseStaffWorkbook(boolean partyDropdown, boolean statusDropdown, boolean statusAsList, + int statusLastRow) throws Exception { + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("员工信息"); + Row header = sheet.createRow(0); + String[] headers = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)", + "入职时间", "是否党员", "状态"}; + for (int i = 0; i < headers.length; i++) { + header.createCell(i).setCellValue(headers[i]); + } + createBaseStaffRow(sheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1"); + createBaseStaffRow(sheet, 2, "李四", 9020002L, "330106198603031022", "1", "1"); + + if (partyDropdown) { + addListValidation(sheet, 7, 1, 2, "0", "1"); + } + if (statusDropdown) { + if (statusAsList) { + addListValidation(sheet, 8, 1, statusLastRow, "0", "1"); + } else { + addIntegerValidation(sheet, 8, 1, 2); + } + } + + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + + private byte[] baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown() throws Exception { + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + Sheet staffSheet = workbook.createSheet("员工信息"); + Row staffHeader = staffSheet.createRow(0); + String[] staffHeaders = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)", + "入职时间", "是否党员", "状态"}; + for (int i = 0; i < staffHeaders.length; i++) { + staffHeader.createCell(i).setCellValue(staffHeaders[i]); + } + createBaseStaffRow(staffSheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1"); + addListValidation(staffSheet, 7, 1, 1, "0", "1"); + addListValidation(staffSheet, 8, 1, 1, "0", "1"); + + Sheet assetSheet = workbook.createSheet("员工资产信息"); + Row assetHeader = assetSheet.createRow(0); + String[] assetHeaders = {"员工身份证号*", "资产大类*", "资产小类*", "资产名称*", "产权占比", + "购买/评估日期", "资产原值", "当前估值*", "估值截止日期", "资产状态*", "备注"}; + for (int i = 0; i < assetHeaders.length; i++) { + assetHeader.createCell(i).setCellValue(assetHeaders[i]); + } + Row assetRow = assetSheet.createRow(1); + assetRow.createCell(0).setCellValue("33010619850202101X"); + assetRow.createCell(1).setCellValue("房产"); + assetRow.createCell(2).setCellValue("住宅"); + assetRow.createCell(3).setCellValue("测试住宅"); + assetRow.createCell(7).setCellValue(1000000D); + assetRow.createCell(9).setCellValue("正常"); + + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + + private void createBaseStaffRow(Sheet sheet, int rowIndex, String name, long staffId, String idCard, + String partyMember, String status) { + Row row = sheet.createRow(rowIndex); + row.createCell(0).setCellValue(name); + row.createCell(1).setCellValue(staffId); + row.createCell(2).setCellValue(103L); + row.createCell(3, CellType.STRING).setCellValue(idCard); + row.createCell(4, CellType.STRING).setCellValue("13370000001"); + row.createCell(5).setCellValue(new BigDecimal("180000").doubleValue()); + row.createCell(6).setCellValue("2026-04-30"); + row.createCell(7, CellType.STRING).setCellValue(partyMember); + row.createCell(8, CellType.STRING).setCellValue(status); + } + + private void addListValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow, String... options) { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = helper.createExplicitListConstraint(options); + DataValidation validation = helper.createValidation( + constraint, + new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex) + ); + sheet.addValidationData(validation); + } + + private void addIntegerValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow) { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = helper.createIntegerConstraint( + DataValidationConstraint.OperatorType.BETWEEN, + "0", + "1" + ); + DataValidation validation = helper.createValidation( + constraint, + new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex) + ); + sheet.addValidationData(validation); + } + + private byte[] plainWorkbookWithoutDropdown() throws Exception { + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("普通信息"); + Row header = sheet.createRow(0); + header.createCell(0).setCellValue("名称"); + Row row = sheet.createRow(1); + row.createCell(0).setCellValue("张三"); + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + + public static class PlainExcel { + @ExcelProperty(value = "名称", index = 0) + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java b/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java new file mode 100644 index 00000000..81623355 --- /dev/null +++ b/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java @@ -0,0 +1,61 @@ +package com.ruoyi.lsfx.client; + +import com.ruoyi.lsfx.constants.LsfxConstants; +import com.ruoyi.lsfx.domain.response.UploadFileResponse; +import com.ruoyi.lsfx.util.HttpUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.Resource; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LsfxAnalysisClientTest { + + @Mock + private HttpUtil httpUtil; + + @InjectMocks + private LsfxAnalysisClient client; + + @TempDir + Path tempDir; + + @Test + void uploadFile_shouldPassOriginalFilenameToMultipartResource() throws Exception { + ReflectionTestUtils.setField(client, "baseUrl", "http://lsfx"); + ReflectionTestUtils.setField(client, "uploadFileEndpoint", "/upload"); + ReflectionTestUtils.setField(client, "clientId", "client-1"); + + Path tempFile = tempDir.resolve("batch_0_123456.xlsx"); + Files.writeString(tempFile, "content"); + + UploadFileResponse response = new UploadFileResponse(); + response.setData(new UploadFileResponse.UploadData()); + + ArgumentCaptor> paramsCaptor = ArgumentCaptor.forClass(Map.class); + ArgumentCaptor> headersCaptor = ArgumentCaptor.forClass(Map.class); + when(httpUtil.uploadFile(eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class))) + .thenReturn(response); + + client.uploadFile(200, tempFile.toFile(), "银行流水A.xlsx"); + + assertEquals(200, paramsCaptor.getValue().get("groupId")); + Resource filePart = assertInstanceOf(Resource.class, paramsCaptor.getValue().get("files")); + assertEquals("银行流水A.xlsx", filePart.getFilename()); + assertEquals("client-1", headersCaptor.getValue().get(LsfxConstants.HEADER_CLIENT_ID)); + } +} diff --git a/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java b/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java new file mode 100644 index 00000000..346455dd --- /dev/null +++ b/ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java @@ -0,0 +1,59 @@ +package com.ruoyi.lsfx.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class HttpUtilTest { + + @Mock + private RestTemplate restTemplate; + + @TempDir + Path tempDir; + + @Test + void uploadFile_shouldUseExplicitResourceFilename() throws Exception { + HttpUtil httpUtil = new HttpUtil(); + ReflectionTestUtils.setField(httpUtil, "restTemplate", restTemplate); + + Path tempFile = tempDir.resolve("batch_0_123456.xlsx"); + Files.writeString(tempFile, "content"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpEntity.class); + when(restTemplate.postForEntity(eq("http://lsfx/upload"), captor.capture(), eq(String.class))) + .thenReturn(ResponseEntity.ok("ok")); + + Map params = new HashMap<>(); + params.put("groupId", 200); + params.put("files", HttpUtil.namedFileResource(tempFile.toFile(), "银行流水A.xlsx")); + + String result = httpUtil.uploadFile("http://lsfx/upload", params, null, String.class); + + assertEquals("ok", result); + MultiValueMap body = (MultiValueMap) captor.getValue().getBody(); + Object filePart = body.getFirst("files"); + Resource resource = assertInstanceOf(Resource.class, filePart); + assertEquals("银行流水A.xlsx", resource.getFilename()); + } +} diff --git a/ccdi-lsfx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/ccdi-lsfx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..fdbd0b15 --- /dev/null +++ b/ccdi-lsfx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-subclass diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagTaskMapperXmlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagTaskMapperXmlTest.java new file mode 100644 index 00000000..afdea95c --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagTaskMapperXmlTest.java @@ -0,0 +1,40 @@ +package com.ruoyi.ccdi.project.mapper; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiBankTagTaskMapperXmlTest { + + private static final String RESOURCE = "mapper/ccdi/project/CcdiBankTagTaskMapper.xml"; + + @Test + void mapper_shouldExposeLatestFailedTaskQuery() { + Method method = Arrays.stream(CcdiBankTagTaskMapper.class.getDeclaredMethods()) + .filter(item -> "selectLatestFailedTaskByProjectId".equals(item.getName())) + .findFirst() + .orElse(null); + + assertNotNull(method, "应提供查询项目最近失败打标任务的方法"); + } + + @Test + void xml_shouldSelectLatestFailedTaskByProjectId() throws Exception { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) { + String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + + assertTrue(xml.contains("selectLatestFailedTaskByProjectId"), xml); + assertTrue(xml.contains("from ccdi_bank_tag_task"), xml); + assertTrue(xml.contains("project_id = #{projectId}"), xml); + assertTrue(xml.contains("status = 'FAILED'"), xml); + assertTrue(xml.contains("order by id desc"), xml); + assertTrue(xml.contains("limit 1"), xml); + } + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectExtendedPurchaseSupplierContractTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectExtendedPurchaseSupplierContractTest.java new file mode 100644 index 00000000..b611f210 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectExtendedPurchaseSupplierContractTest.java @@ -0,0 +1,27 @@ +package com.ruoyi.ccdi.project.mapper; + +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiProjectExtendedPurchaseSupplierContractTest { + + @Test + void shouldExposeExtendedPurchaseSupplierDetailQuery() throws Exception { + Class mapperClass = Class.forName("com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper"); + + Method method = mapperClass.getMethod("selectExtendedPurchaseSuppliers", Long.class, String.class); + assertEquals(List.class, method.getReturnType()); + + String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml")); + assertTrue(xml.contains("select id=\"selectExtendedPurchaseSuppliers\"")); + assertTrue(xml.contains("ccdi_purchase_transaction_supplier")); + assertTrue(xml.contains("is_bid_winner")); + assertTrue(xml.contains("sort_order")); + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapperXmlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapperXmlTest.java new file mode 100644 index 00000000..2ca84051 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapperXmlTest.java @@ -0,0 +1,51 @@ +package com.ruoyi.ccdi.project.mapper; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiProjectMapperXmlTest { + + @Test + void projectQueriesShouldApplyProjectVisibilityAndDeletedFilters() throws Exception { + String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml")); + + assertFalse(xml.contains("params.dataScope"), xml); + assertFalse(xml.contains("${queryDTO.params.dataScope}"), xml); + + String listSql = extractSelect(xml, "selectProjectPage"); + assertTrue(listSql.contains("LEFT JOIN sys_user u ON p.create_by = u.user_name"), listSql); + assertTrue(listSql.contains("scope != null and !scope.viewAllProjects"), listSql); + assertTrue(listSql.contains("p.create_by = #{scope.username}"), listSql); + assertTrue(listSql.contains("queryDTO.includeDeleted"), listSql); + assertTrue(listSql.contains("p.del_flag = '2'"), listSql); + assertTrue(listSql.contains("p.status = '5'"), listSql); + assertTrue(listSql.contains("p.del_flag = '0'"), listSql); + assertTrue(listSql.contains("p.status != '5'"), listSql); + + String historySql = extractSelect(xml, "selectHistoryProjects"); + assertTrue(historySql.contains("p.status in ('1', '2')"), historySql); + assertTrue(historySql.contains("p.del_flag = '0'"), historySql); + + assertTrue(xml.contains(""), xml); + assertTrue(xml.contains("status = '5'"), xml); + assertTrue(xml.contains("del_flag = '2'"), xml); + assertTrue(xml.contains(""), xml); + assertTrue(xml.contains("status = '1'"), xml); + assertTrue(xml.contains("del_flag = '0'"), xml); + assertTrue(xml.contains("is_archived = 0"), xml); + } + + private String extractSelect(String xml, String selectId) { + String start = "", startIndex); + assertTrue(endIndex >= 0, "missing closing select tag: " + selectId); + return xml.substring(startIndex, endIndex); + } +} diff --git a/ccdi-project/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/ccdi-project/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..fdbd0b15 --- /dev/null +++ b/ccdi-project/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-subclass diff --git a/docs/tests/records/2026-04-21-intermediary-import-browser-test-record.md b/docs/tests/records/2026-04-21-intermediary-import-browser-test-record.md new file mode 100644 index 00000000..700355b1 --- /dev/null +++ b/docs/tests/records/2026-04-21-intermediary-import-browser-test-record.md @@ -0,0 +1,138 @@ +# 2026-04-21 中介库管理导入功能浏览器测试记录 + +## 1. 测试目标 + +- 在真实浏览器中验证“中介库管理”页面两类导入功能: + - 导入中介和亲属信息 + - 导入中介实体关联关系 +- 验证模板下载、模板改写、页面上传、异步导入结果展示、失败记录查看全链路可用。 + +## 2. 测试环境 + +- 前端地址:`http://localhost:62319` +- 后端地址:`http://127.0.0.1:62318` +- 登录方式:调用项目测试登录接口 `/login/test` 获取 token 后注入浏览器 Cookie +- 浏览器:Playwright headed 模式 + +## 3. 下载模板 + +- 中介和亲属信息模板: + - 浏览器下载原始文件:`.playwright-cli/中介和亲属信息导入模板-1776735200748.xlsx` +- 中介实体关联关系模板: + - 浏览器下载原始文件:`.playwright-cli/中介实体关联关系导入模板-1776735620019.xlsx` + +## 4. 生成测试文件 + +- 中介和亲属信息首轮混合测试文件: + - `output/spreadsheet/intermediary_person_import_browser_phase1.xlsx` +- 中介和亲属信息二轮库内重复测试文件: + - `output/spreadsheet/intermediary_person_import_browser_phase2_existing_db_cases.xlsx` +- 中介实体关联关系首轮混合测试文件: + - `output/spreadsheet/intermediary_enterprise_relation_import_browser_phase1.xlsx` +- 中介实体关联关系二轮库内重复测试文件: + - `output/spreadsheet/intermediary_enterprise_relation_import_browser_phase2_db_duplicate.xlsx` + +## 5. 页面测试过程与结果 + +### 5.1 导入中介和亲属信息 + +#### 第一轮:混合成功/失败场景 + +- 上传文件:`output/spreadsheet/intermediary_person_import_browser_phase1.xlsx` +- 页面导入结果: + - 总数 `13` 条 + - 成功 `4` 条 + - 失败 `9` 条 +- 页面列表确认新增成功数据: + - `自动化中介本人A` + - `自动化中介A配偶` + - `文件内重复本人1` + - `文件内重复亲属1` + +#### 第一轮失败记录命中情况 + +- `本人行关联中介本人证件号码必须为空` +- `亲属行必须填写关联中介本人证件号码` +- `姓名不能为空` +- `人员子类型不能为空` +- `证件号码身份证号长度必须为18位` +- `中介本人证件号码[320101199503154560]在导入文件中重复` +- `关联中介本人证件号码[320101197704077654]不存在` +- `同一中介本人名下证件号码[320101199604102348]的亲属在导入文件中重复` + +说明: + +- 原计划用于命中“库内本人重复”的旧数据 `320101199904170833` 因系统内该历史证件号本身校验位不合法,页面实际先命中了证件号格式校验。 + +#### 第二轮:补齐库内重复分支 + +- 上传文件:`output/spreadsheet/intermediary_person_import_browser_phase2_existing_db_cases.xlsx` +- 页面导入结果: + - 总数 `2` 条 + - 成功 `0` 条 + - 失败 `2` 条 +- 页面失败记录确认命中: + - `中介本人证件号码[46265019770816746X]已存在,请勿重复导入` + - `同一中介本人名下证件号码[320101199902033213]的亲属已存在,请勿重复导入` + +### 5.2 导入中介实体关联关系 + +#### 第一轮:混合成功/失败场景 + +- 上传文件:`output/spreadsheet/intermediary_enterprise_relation_import_browser_phase1.xlsx` +- 页面导入结果: + - 总数 `11` 条 + - 成功 `3` 条 + - 失败 `8` 条 +- 页面列表确认新增成功数据: + - `成都市资产企业 / 自动化中介本人A / 董事` + - `上海市资产企业 / 自动化中介本人A / 监事` + - `杭州市不动产合伙企业 / 自动化中介本人A / 法人` + +#### 第一轮失败记录命中情况 + +- `中介本人证件号码不能为空` +- `中介本人证件号码身份证号长度必须为18位` +- `中介本人不存在,请先导入或维护中介本人信息` +- `统一社会信用代码不能为空` +- `统一社会信用代码不存在于系统机构表` +- `关联人职务长度不能超过100个字符` +- `备注长度不能超过500个字符` +- `同一中介本人与统一社会信用代码组合在导入文件中重复` + +#### 第二轮:补齐库内关系重复分支 + +- 上传文件:`output/spreadsheet/intermediary_enterprise_relation_import_browser_phase2_db_duplicate.xlsx` +- 页面导入结果: + - 总数 `1` 条 + - 成功 `0` 条 + - 失败 `1` 条 +- 页面失败记录确认命中: + - `中介实体关联关系已存在,请勿重复导入` + +## 6. 结论 + +- 两类导入功能的模板下载、模板改写、页面上传、异步轮询、失败记录弹窗均可正常工作。 +- 中介和亲属信息导入已覆盖: + - 成功导入 + - 本人行关联字段错误 + - 亲属缺少关联本人 + - 姓名为空 + - 人员子类型为空 + - 证件号非法 + - 文件内本人重复 + - 关联本人不存在 + - 文件内亲属重复 + - 库内本人重复 + - 库内亲属重复 +- 中介实体关联关系导入已覆盖: + - 成功导入 + - 中介本人为空 + - 中介本人证件号非法 + - 中介本人不存在 + - 统一社会信用代码为空 + - 统一社会信用代码不存在 + - 关联人职务超长 + - 备注超长 + - 文件内关系重复 + - 库内关系重复 diff --git a/docs/tests/records/2026-04-22-base-staff-maintenance-browser-test-record.md b/docs/tests/records/2026-04-22-base-staff-maintenance-browser-test-record.md new file mode 100644 index 00000000..379f0b10 --- /dev/null +++ b/docs/tests/records/2026-04-22-base-staff-maintenance-browser-test-record.md @@ -0,0 +1,196 @@ +# 2026-04-22 员工信息维护真实页面综合测试记录 + +## 1. 测试目标 + +- 在真实浏览器中进入“信息维护-员工信息维护”页面,验证页面可访问、列表可加载、关键接口可正常返回。 +- 覆盖员工信息维护核心业务链路: + - 列表加载 + - 详情查看 + - 新增 + - 编辑 + - 删除 + - 导入模板下载 + - 双 Sheet Excel 导入 + - 员工导入失败记录查看 + - 员工资产导入失败记录查看 +- 测试结束后清理本轮新增/导入成功数据,并清理页面本地导入缓存。 + +## 2. 测试环境 + +- 前端地址:`http://127.0.0.1:1025` +- 后端地址:`http://127.0.0.1:62318` +- Mock 服务:`http://127.0.0.1:8000` +- 前端 Node 版本:`nvm use 14.21.3` +- 浏览器:Playwright headed 模式真实浏览器 +- 登录方式:先调用测试登录接口获取 token,再向真实浏览器注入 `Admin-Token` cookie +- 测试页面:`http://127.0.0.1:1025/maintain/baseStaff` + +## 3. 测试文件 + +- 页面下载模板: + - `/Users/wkc/Desktop/ccdi/ccdi/output/playwright/base-staff-maintenance-test/.playwright-cli/员工信息维护导入模板-1776851234493.xlsx` +- 生成导入样本: + - `/Users/wkc/Desktop/ccdi/ccdi/output/spreadsheet/base_staff_import_browser_mixed.xlsx` +- Playwright 上传副本: + - `/Users/wkc/Desktop/ccdi/ccdi/output/playwright/base-staff-maintenance-test/base_staff_import_browser_mixed.xlsx` + +## 4. 页面测试过程与结果 + +### 4.1 页面进入与基础加载 + +- 进入“信息维护-员工信息维护”页面后,菜单高亮、面包屑、查询区、列表区展示正常。 +- Playwright 网络面板确认关键请求返回 `200`: + - `/dev-api/getInfo` + - `/dev-api/getRouters` + - `/dev-api/ccdi/baseStaff/list` + - `/dev-api/system/user/deptTree` +- 页面列表初始总数为: + - `共 1014 条` + +### 4.2 新增 + +- 新增员工: + - 姓名:`浏览器员工测试A-0422` + - 柜员号:`9260422` + - 所属部门:`若依科技` + - 身份证号:`330101199206150012` + - 电话:`13926042222` + - 年收入:`246800.50` + - 入职时间:`2026-04-22` + - 是否党员:`是` + - 状态:`在职` +- 同时新增两条资产: + - `房产 / 住宅 / 浏览器测试住房A / 当前估值 860000 / 状态 正常 / 备注 新增资产A` + - `车辆 / 轿车 / 浏览器测试车辆B / 当前估值 188000 / 状态 正常 / 备注 新增资产B` +- 保存后验证通过: + - 新增员工进入列表首行 + - 列表总数从 `1014` 增加到 `1015` + +### 4.3 新增后详情复验 + +- 打开新增员工详情后验证通过: + - 基本信息可正常展示 + - 资产明细展示 2 条 + - 资产名称、估值、状态、备注与录入一致 + +### 4.4 编辑 + +- 编辑同一员工,修改内容: + - 姓名改为 `浏览器员工测试A-0422-已改` + - 电话改为 `13926042224` + - 年收入改为 `268000` + - 删除“房产 / 住宅”资产,仅保留“车辆 / 轿车”资产 + - 保留资产状态改为 `冻结` + - 保留资产备注改为 `编辑后保留资产` +- 保存后验证通过: + - 列表首行姓名、电话、年收入已同步更新 + - 详情中仅剩 1 条原有资产 + - 保留资产状态显示 `冻结` + - 保留资产备注显示 `编辑后保留资产` + +### 4.5 导入模板下载 + +- 在真实页面导入弹窗中点击“下载模板”。 +- 下载结果正常,模板包含两个 Sheet: + - `员工信息` + - `员工资产信息` + +### 4.6 双 Sheet 导入 + +- 使用 `base_staff_import_browser_mixed.xlsx` 发起双 Sheet 导入。 +- 员工信息 Sheet: + - 成功样本:`浏览器导入员工B-0422 / 9260423` + - 失败样本:`浏览器导入失败员工-0422 / 9033101` +- 员工资产信息 Sheet: + - 成功样本:向员工 `330101199206150012` 导入资产 `导入资产车位A-0422` + - 失败样本:使用不存在证件号 `330101199206150063` +- 导入提交后页面提示: + - `导入任务已提交` +- 导入完成后验证通过: + - 页面出现“查看导入失败记录” + - 页面出现“查看员工资产导入失败记录” + - 成功导入员工 `9260423` 进入列表 + - 列表总数从 `1015` 增加到 `1016` + +### 4.7 员工导入失败记录 + +- 打开“查看导入失败记录”弹窗。 +- 页面失败记录命中: + - Sheet:`员工信息` + - Excel 行号:`3` + - 姓名:`浏览器导入失败员工-0422` + - 柜员号:`9033101` + - 失败原因:`该员工ID已存在` +- 说明员工导入失败记录展示链路正常。 + +### 4.8 员工资产导入失败记录 + +- 打开“查看员工资产导入失败记录”弹窗。 +- 页面失败记录命中: + - Sheet:`员工资产信息` + - Excel 行号:`3` + - 资产实际持有人身份证号:`330101199206150063` + - 资产名称:`导入失败资产-0422` + - 失败原因:`员工资产导入仅支持员工本人证件号` +- 说明员工资产导入失败记录展示链路正常。 + +### 4.9 导入成功数据复验 + +- 再次打开员工 `9260422` 的详情。 +- 验证通过: + - 新导入资产 `房产 / 车位 / 导入资产车位A-0422` 已展示 + - 当前估值显示 `235000` + - 资产状态显示 `正常` + - 备注显示 `页面上传成功资产` + - 编辑后保留的车辆资产仍存在 + +### 4.10 删除与清理 + +- 通过真实页面删除以下两条本轮测试产生的成功数据: + - 导入成功员工:`9260423` + - 页面新增员工:`9260422` +- 删除过程均经过二次确认弹窗,页面提示 `删除成功`。 +- 清理完成后验证通过: + - 列表总数回到 `共 1014 条` + - 页面中已无 `9260422`、`9260423` +- 清理本地导入任务缓存: + - 已移除 `localStorage` 中的 `employee_import_last_task` + - 已移除 `localStorage` 中的 `employee_asset_import_last_task` +- 页面刷新后: + - “查看导入失败记录”按钮已消失 + - “查看员工资产导入失败记录”按钮已消失 + +## 5. 发现的问题 + +- 员工详情弹窗“所属部门”显示异常。 +- 复现方式: + - 新增或编辑员工后,列表中“所属部门”正常显示为 `若依科技` + - 打开同一员工详情,字段“所属部门”显示为 `-` +- 影响判断: + - 属于真实功能问题,影响详情信息一致性 + - 不影响本次新增、编辑、导入、删除主流程完成 + +## 6. 结论 + +- 员工信息维护模块在真实页面下的核心链路已完整验证: + - 列表加载 + - 详情查看 + - 新增 + - 编辑 + - 删除 + - 导入模板下载 + - 双 Sheet 导入 + - 员工导入失败记录查看 + - 员工资产导入失败记录查看 +- 本轮测试发现 1 个非阻断性功能问题: + - 员工详情弹窗所属部门未正确回显 +- 本轮新增和导入成功数据已全部清理,页面本地导入缓存已清空。 + +## 7. 测试收尾 + +- 已清理本轮新增/导入成功业务数据。 +- 已清理前端本地导入任务缓存。 +- 待测试完成后关闭: + - Playwright 浏览器会话 + - 前端 `npm run dev -- --port 1025` 进程 + - 本轮通过 `bin/restart_java_backend.sh` 拉起的后端进程 diff --git a/docs/tests/records/2026-04-22-bidding-info-maintenance-browser-test-record.md b/docs/tests/records/2026-04-22-bidding-info-maintenance-browser-test-record.md new file mode 100644 index 00000000..ae1627f2 --- /dev/null +++ b/docs/tests/records/2026-04-22-bidding-info-maintenance-browser-test-record.md @@ -0,0 +1,170 @@ +# 2026-04-22 招投标信息维护真实页面综合测试记录 + +## 1. 测试目标 + +- 在真实浏览器中进入“信息维护-招投标信息维护”页面,验证页面可访问、列表可加载、无明显前端报错。 +- 覆盖招投标信息维护核心业务链路: + - 列表加载 + - 条件查询 + - 详情查看 + - 新增 + - 编辑 + - 删除 + - 导入模板下载 + - 双 Sheet Excel 导入 + - 导入失败记录查看 +- 测试结束后清理本轮新增/导入成功数据,并关闭测试过程启动的前端进程与浏览器会话。 + +## 2. 测试环境 + +- 前端地址:`http://localhost:62319` +- 后端地址:`http://127.0.0.1:62318` +- Mock 服务:`http://127.0.0.1:8000` +- 前端 Node 版本:`nvm use 14.21.3` +- 浏览器:Playwright headed 模式真实浏览器 +- 登录方式:通过真实登录页使用预填测试账号 `admin / admin123` 登录后进入页面 +- 测试页面:`http://localhost:62319/maintain/purchaseTransaction` + +## 3. 测试文件 + +- 页面下载模板: + - `/Users/wkc/Desktop/ccdi/ccdi/output/playwright/bidding-maintenance-test/.playwright-cli/招投标信息维护导入模板-1776842565558.xlsx` +- 生成导入样本: + - `/Users/wkc/Desktop/ccdi/ccdi/output/spreadsheet/bidding_info_import_browser_mixed.xlsx` +- Playwright 上传副本: + - `/Users/wkc/Desktop/ccdi/ccdi/output/playwright/bidding-maintenance-test/bidding_info_import_browser_mixed.xlsx` + +## 4. 页面测试过程与结果 + +### 4.1 页面进入与基础加载 + +- 从真实登录页登录后,通过顶部菜单进入“信息维护-招投标信息维护”。 +- 页面面包屑、菜单高亮、列表列头显示正常。 +- 关键列表列正常展示: + - `中标供应商` + - `参与供应商数` + - `预算金额(元)` +- Playwright 网络面板确认首屏关键请求均返回 `200`: + - `/dev-api/getInfo` + - `/dev-api/getRouters` + - `/dev-api/ccdi/purchaseTransaction/list` +- Playwright 控制台未发现 warning。 + +### 4.2 详情查看 + +- 打开首条现有记录详情: + - `LSFXMOCKP2PUR001` +- 验证通过: + - 基本信息、金额信息、重要日期、申请人信息正常展示 + - 供应商明细表正常展示 + - 供应商排序、中标标识、统一信用代码、联系人、联系电话、银行账户展示正确 + +### 4.3 新增 + +- 新增测试单据: + - 采购事项ID:`AUTOBID20260422151036` + - 项目名称:`自动化招投标测试项目-1036` +- 新增时录入两条供应商: + - `杭州自动化供应商A有限公司` + - `杭州自动化供应商B有限公司` +- 将第 2 条供应商标记为中标后提交成功。 +- 列表校验通过: + - 新增记录进入列表首行 + - `中标供应商` 显示为 `杭州自动化供应商B有限公司` + - `参与供应商数` 显示为 `2` + - `预算金额(元)`、申请人、申请部门、申请日期与录入一致 + +### 4.4 新增后详情复验 + +- 打开新增记录详情后验证通过: + - 两条供应商都在详情表中展示 + - 第 1 条显示 `参标` + - 第 2 条显示 `中标` + - 排序、联系人、联系电话、银行账户与录入一致 + - 全部重要日期、采购负责人信息正确回显 + +### 4.5 编辑 + +- 编辑同一条测试单据,修改内容: + - 项目名称改为 `自动化招投标测试项目-1036-已改` + - 标的物名称改为 `服务器及配件-1036-复核` + - 预算金额改为 `258000` + - 将中标供应商从 `杭州自动化供应商B有限公司` 切换为 `杭州自动化供应商A有限公司` +- 保存后列表校验通过: + - 项目名称、标的物名称、预算金额已更新 + - `中标供应商` 摘要同步更新为 `杭州自动化供应商A有限公司` + - `参与供应商数` 仍为 `2` + +### 4.6 条件查询 + +- 以编辑后的项目名称 `自动化招投标测试项目-1036-已改` 进行查询。 +- 查询结果: + - 分页总数显示 `共 1 条` + - 列表首条采购事项ID为 `AUTOBID20260422151036` +- 说明项目名称条件查询可正常命中目标记录。 + +### 4.7 导入模板下载 + +- 在真实页面导入弹窗中点击“下载模板”。 +- 下载结果正常,模板包含两个 Sheet: + - `招投标主信息` + - `供应商明细` + +### 4.8 导入 + +- 使用自制双 Sheet 测试文件 `bidding_info_import_browser_mixed.xlsx` 发起导入。 +- 测试文件包含 2 个采购事项ID: + - 成功样本:`IMPBID20260422152321A` + - 失败样本:`IMPBID20260422152321B` +- 导入后页面结果验证通过: + - 成功样本进入列表 + - 其 `中标供应商` 显示为 `导入供应商乙有限公司` + - 其 `参与供应商数` 显示为 `2` + - 页面出现“查看导入失败记录”按钮 + +### 4.9 导入失败记录 + +- 打开“查看导入失败记录”弹窗。 +- 页面失败记录命中: + - 采购事项ID:`IMPBID20260422152321B` + - 项目名称:`导入失败项目-152321` + - 标的物名称:`失败样例设备-152321` + - 失败原因:`申请人工号必须为7位数字` +- 说明导入失败记录分页、失败原因展示链路正常。 + +### 4.10 删除与清理 + +- 通过真实页面删除以下两条本轮测试产生的成功数据: + - `AUTOBID20260422151036` + - `IMPBID20260422152321A` +- 删除时均经过页面二次确认弹窗,删除成功后搜索结果回到 `共 0 条`。 +- 清理本地导入任务缓存: + - 已移除 `localStorage` 中的 `purchase_transaction_import_last_task` +- 页面刷新后,“查看导入失败记录”按钮已消失。 +- 最终列表总数恢复为: + - `共 2004 条` + +## 5. 结论 + +- 招投标信息维护模块在真实页面下的核心链路测试通过。 +- 本次已覆盖: + - 页面进入与真实登录 + - 列表展示 + - 条件查询 + - 详情查看 + - 新增 + - 编辑 + - 删除 + - 导入模板下载 + - 双 Sheet 导入 + - 导入失败记录查看 +- 本轮测试未发现阻断性缺陷。 +- 本轮新增和导入成功的数据已清理,列表总数已回到测试前水平。 + +## 6. 测试收尾 + +- 已清理本轮新增/导入成功业务数据。 +- 已清理前端本地导入任务缓存。 +- 待测试完成后关闭: + - Playwright 浏览器会话 + - 前端 `npm run dev` 进程 diff --git a/docs/tests/records/2026-04-24-staff-family-enterprise-relation-browser-test-record.md b/docs/tests/records/2026-04-24-staff-family-enterprise-relation-browser-test-record.md new file mode 100644 index 00000000..04a00ba9 --- /dev/null +++ b/docs/tests/records/2026-04-24-staff-family-enterprise-relation-browser-test-record.md @@ -0,0 +1,50 @@ +# 员工亲属实体关联真实页面测试记录 + +## 测试时间 + +2026-04-24 13:32 + +## 测试范围 + +- 页面:员工亲属实体关联 +- 实际访问地址:`http://localhost:8080/maintain/staffEnterpriseRelation` +- 后端接口代理:`http://localhost:8080/dev-api` +- 登录账号:`admin` + +## 环境信息 + +- 后端:本地 `62318` 端口已启动 +- 前端:`ruoyi-ui` 使用 `.nvmrc` 指定的 Node `14.21.3`,执行 `npm run dev -- --port 8080 --open false` +- 浏览器:Playwright 启动本机 Google Chrome,访问真实业务页面 + +## 测试数据 + +- 选择的有效亲属:`320101199201010051 LSFX低收入亲属 / 导入验证员工20260317` +- 测试统一社会信用代码:`91756899ABCDEFGH12` +- 测试企业名称:`自动化测试亲属实体企业56899` +- 测试职务:`自动化测试职务` +- 编辑后职务:`自动化测试职务-已编辑` + +## 测试步骤与结果 + +1. 打开真实业务页面,确认员工亲属实体关联列表加载成功。 +2. 点击“新增”,通过亲属身份证号下拉选择有效员工亲属,确认亲属姓名与关联员工自动带出。 +3. 填写统一社会信用代码、企业名称、职务、补充说明并提交,页面提示“新增成功”。 +4. 按企业名称搜索,确认新增记录展示在列表中,并包含企业名称、职务和关联员工信息。 +5. 打开“详情”,确认详情弹窗展示亲属身份证号、亲属姓名、关联员工、统一社会信用代码、企业名称、职务、状态、数据来源和补充说明。 +6. 点击“编辑”,修改职务后提交,页面提示“修改成功”,列表展示编辑后的职务。 +7. 再次新增同一亲属身份证号与同一统一社会信用代码组合,确认页面拦截并提示“该亲属身份证号和统一社会信用代码组合已存在”。 +8. 删除本轮新增记录,页面提示“删除成功”,完成测试数据清理。 +9. 测试结束后通过后端列表接口按测试企业名称复核,返回 `total = 0`,确认本轮测试数据已清理。 + +## 验证结论 + +- 员工亲属实体关联主链路通过。 +- 有效亲属下拉搜索、自动带出关联员工、新增、查询、详情、编辑、重复组合校验、删除清理均符合预期。 +- 测试过程中未发现浏览器控制台错误。 +- 测试过程中未发现失败请求。 + +## 测试产物 + +- 页面截图位于 `output/playwright/`,文件名前缀为 `staff-family-enterprise-*2026-04-24T05-32-31-510Z.png`。 +- 临时 Playwright 脚本位于 `output/playwright/staff-family-enterprise-relation-browser-test.js`,该目录为测试输出目录,不纳入 Git 提交范围。 diff --git a/docs/tests/records/2026-04-24-staff-family-enterprise-relation-import-browser-test-record.md b/docs/tests/records/2026-04-24-staff-family-enterprise-relation-import-browser-test-record.md new file mode 100644 index 00000000..261e3500 --- /dev/null +++ b/docs/tests/records/2026-04-24-staff-family-enterprise-relation-import-browser-test-record.md @@ -0,0 +1,60 @@ +# 员工亲属实体关联导入真实页面测试记录 + +## 测试时间 + +2026-04-24 13:38 + +## 测试范围 + +- 页面:员工亲属实体关联 +- 实际访问地址:`http://localhost:8080/maintain/staffEnterpriseRelation` +- 导入接口:`/ccdi/staffEnterpriseRelation/importData` +- 导入状态接口:`/ccdi/staffEnterpriseRelation/importStatus/{taskId}` +- 失败记录接口:`/ccdi/staffEnterpriseRelation/importFailures/{taskId}` + +## 环境信息 + +- 后端:本地 `62318` 端口既有进程 +- 前端:`ruoyi-ui` 使用 `.nvmrc` 指定的 Node `14.21.3`,执行 `npm run dev -- --port 8080 --open false` +- 浏览器:Playwright 启动本机 Google Chrome,访问真实业务页面 + +## 测试数据 + +- 有效亲属:`320101199201010051 LSFX低收入亲属 / 导入验证员工20260317` +- 成功记录统一社会信用代码:`91141873ABCDEFGH12` +- 成功记录企业名称:`导入测试亲属实体企业141873` +- 成功记录职务:`导入测试职务` +- 失败记录场景:同一导入文件内重复 `亲属身份证号 + 统一社会信用代码` 组合 + +## 测试步骤与结果 + +1. 打开真实业务页面,点击“导入”,确认弹出“员工亲属实体关联数据导入”弹窗。 +2. 在导入弹窗中点击“下载模板”,下载当前页面模板文件。 +3. 基于下载的模板生成测试文件,写入两条数据: + - 第 1 条为有效亲属 + 唯一统一社会信用代码,预期成功。 + - 第 2 条与第 1 条使用相同亲属身份证号和统一社会信用代码,预期文件内重复失败。 +4. 在真实页面上传该测试文件并点击“确定”,页面提示“员工亲属实体关联导入任务已提交”。 +5. 等待页面异步轮询完成,出现“查看导入失败记录”按钮,说明本轮为部分成功。 +6. 按成功记录企业名称查询列表,确认成功导入记录展示在列表中。 +7. 打开成功记录详情,确认统一社会信用代码 `91141873ABCDEFGH12` 与导入来源展示正确。 +8. 点击“查看导入失败记录”,确认失败记录弹窗展示重复组合错误:`组合在导入文件中重复`。 +9. 删除本轮成功导入记录,页面提示“删除成功”。 +10. 测试结束后通过后端列表接口按测试企业名称复核,返回 `total = 0`,确认本轮成功写入数据已清理。 + +## 验证结论 + +- 导入模板下载正常。 +- 基于当前模板生成的导入文件可被页面上传并提交。 +- 异步导入轮询、部分成功提示、失败记录按钮恢复与失败记录弹窗均符合预期。 +- 成功记录可入库并在列表/详情中查询到。 +- 文件内重复组合被正确拦截,失败原因展示准确。 +- 测试过程中未发现浏览器控制台错误。 +- 测试过程中未发现失败请求。 +- 本轮成功写入的测试数据已删除清理。 + +## 测试产物 + +- 下载模板:`output/playwright/staff-family-enterprise-import-template-2026-04-24T05-38-57-495Z.xlsx` +- 导入样本:`output/playwright/staff-family-enterprise-import-sample-2026-04-24T05-38-57-495Z.xlsx` +- 页面截图位于 `output/playwright/`,文件名前缀为 `staff-family-enterprise-import-*2026-04-24T05-38-57-495Z.png`。 +- 临时 Playwright 脚本位于 `output/playwright/staff-family-enterprise-relation-import-browser-test.js`,该目录为测试输出目录,不纳入 Git 提交范围。 diff --git a/docs/tests/records/2026-04-26-staff-family-enterprise-relation-import-all-cases-browser-test-record.md b/docs/tests/records/2026-04-26-staff-family-enterprise-relation-import-all-cases-browser-test-record.md new file mode 100644 index 00000000..bb7fe55d --- /dev/null +++ b/docs/tests/records/2026-04-26-staff-family-enterprise-relation-import-all-cases-browser-test-record.md @@ -0,0 +1,84 @@ +# 员工亲属实体关联导入全场景真实页面测试记录 + +## 测试时间 + +2026-04-26 16:02 + +## 测试范围 + +- 页面:员工亲属实体关联 +- 实际访问地址:`http://localhost:8080/maintain/staffEnterpriseRelation` +- 导入接口:`/ccdi/staffEnterpriseRelation/importData` +- 导入状态接口:`/ccdi/staffEnterpriseRelation/importStatus/{taskId}` +- 失败记录接口:`/ccdi/staffEnterpriseRelation/importFailures/{taskId}` + +## 环境信息 + +- 后端:本地 `62318` 端口既有进程 +- 前端:`ruoyi-ui` 使用 `.nvmrc` 指定的 Node `14.21.3`,执行 `npm run dev -- --port 8080 --open false` +- 浏览器:Playwright 启动本机 Google Chrome,访问真实业务页面 + +## 前置数据 + +- 有效亲属:`320101199201010051 LSFX低收入亲属 / 导入验证员工20260317` +- 临时无效亲属:`320101199801010421` +- 测试企业名前缀:`全场景导入测试企业520344` + +## 覆盖场景 + +1. 空模板 + - 行数:0 + - 预期:页面提示 `至少需要一条数据` + - 结果:通过 + +2. 基础校验失败 + - 行数:9 + - 覆盖: + - 亲属身份证号为空 + - 统一社会信用代码为空 + - 企业名称为空 + - 亲属身份证号格式错误 + - 统一社会信用代码格式错误 + - 亲属身份证号不存在 + - 亲属身份证号不是有效员工亲属 + - 企业名称长度超过 200 个字符 + - 关联人在企业的职务长度超过 100 个字符 + - 结果:成功 0 条,失败 9 条,失败记录弹窗逐项展示对应原因 + +3. 全成功 + - 行数:3 + - 结果:成功 3 条,失败 0 条,列表可查询到成功导入记录 + +4. 成功失败混合 + - 行数:5 + - 覆盖: + - 2 条有效数据成功导入 + - 文件内重复 `亲属身份证号 + 统一社会信用代码` 组合 + - 亲属身份证号不存在 + - 统一社会信用代码格式错误 + - 结果:成功 2 条,失败 3 条,成功记录可查询,失败记录弹窗展示对应原因 + +5. 已存在组合 + - 行数:1 + - 前置:使用混合场景已成功导入的组合 + - 预期:拦截已存在的 `亲属身份证号 + 统一社会信用代码` 组合 + - 结果:成功 0 条,失败 1 条,失败记录展示 `组合已存在,请勿重复导入` + +## 验证结论 + +- 导入模板下载正常。 +- 基于当前页面模板生成的多份测试文件均可在真实页面上传。 +- 空模板、基础字段校验、有效亲属校验、文件内重复、库内已存在组合、全成功、成功失败混合均已覆盖。 +- 异步导入轮询、成功统计、失败统计、失败记录按钮和失败记录弹窗均符合预期。 +- 成功导入记录能够在列表查询到。 +- 本轮成功写入的 5 条测试数据已删除。 +- 本轮临时新增的无效亲属前置数据已删除。 +- 后端接口复核测试企业名前缀返回 `total = 0`。 +- 浏览器控制台无错误。 +- 测试过程中无失败请求。 + +## 测试产物 + +- 下载模板:`output/playwright/staff-family-enterprise-allcases-template-2026-04-26T08-02-00-344Z.xlsx` +- 场景样本文件与截图位于 `output/playwright/`,文件名前缀为 `staff-family-enterprise-allcases-*2026-04-26T08-02-00-344Z`。 +- 临时 Playwright 脚本位于 `output/playwright/staff-family-enterprise-relation-import-all-cases-browser-test.js`,该目录为测试输出目录,不纳入 Git 提交范围。 diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/PasswordStrengthUtilsTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/PasswordStrengthUtilsTest.java new file mode 100644 index 00000000..bbf4554d --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/PasswordStrengthUtilsTest.java @@ -0,0 +1,37 @@ +package com.ruoyi.web.controller.system; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import com.ruoyi.common.utils.PasswordStrengthUtils; + +class PasswordStrengthUtilsTest +{ + @Test + void shouldAcceptStrongPassword() + { + assertTrue(PasswordStrengthUtils.isStrongPassword("Abc123@#")); + } + + @Test + void shouldRejectPasswordWithoutRequiredCharacterTypes() + { + assertFalse(PasswordStrengthUtils.isStrongPassword("abc123@#")); + assertFalse(PasswordStrengthUtils.isStrongPassword("ABC123@#")); + assertFalse(PasswordStrengthUtils.isStrongPassword("Abcdef@#")); + assertFalse(PasswordStrengthUtils.isStrongPassword("Abc12345")); + } + + @Test + void shouldRejectPasswordWithInvalidLengthOrIllegalCharacters() + { + assertFalse(PasswordStrengthUtils.isStrongPassword(null)); + assertFalse(PasswordStrengthUtils.isStrongPassword("Ab1@")); + assertFalse(PasswordStrengthUtils.isStrongPassword("Abc1234567890123456@#")); + assertFalse(PasswordStrengthUtils.isStrongPassword("Abc 123@")); + assertFalse(PasswordStrengthUtils.isStrongPassword("Abc123<@")); + assertFalse(PasswordStrengthUtils.isStrongPassword("Abc123\\@")); + assertFalse(PasswordStrengthUtils.isStrongPassword("Abc123|@")); + } +} diff --git a/ruoyi-framework/src/test/java/com/ruoyi/framework/config/RedisConfigTest.java b/ruoyi-framework/src/test/java/com/ruoyi/framework/config/RedisConfigTest.java new file mode 100644 index 00000000..43fcd61c --- /dev/null +++ b/ruoyi-framework/src/test/java/com/ruoyi/framework/config/RedisConfigTest.java @@ -0,0 +1,23 @@ +package com.ruoyi.framework.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; + +class RedisConfigTest +{ + @Test + void shouldEnableValidateConnectionForLettuceConnectionFactory() + { + BeanPostProcessor beanPostProcessor = RedisConfig.lettuceConnectionFactoryBeanPostProcessor(); + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory( + new RedisStandaloneConfiguration("127.0.0.1", 6379)); + + beanPostProcessor.postProcessBeforeInitialization(connectionFactory, "redisConnectionFactory"); + + assertThat(connectionFactory.getValidateConnection()).isTrue(); + } +} diff --git a/ruoyi-ui/tests/unit/project-create-upload-jump.test.js b/ruoyi-ui/tests/unit/project-create-upload-jump.test.js new file mode 100644 index 00000000..ead9f303 --- /dev/null +++ b/ruoyi-ui/tests/unit/project-create-upload-jump.test.js @@ -0,0 +1,29 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const indexPath = path.resolve(__dirname, "../../src/views/ccdiProject/index.vue"); +const indexSource = fs.readFileSync(indexPath, "utf8"); + +assert( + indexSource.includes("handleSubmitProject(data)"), + "项目创建成功回调应接收创建接口返回的数据" +); + +assert( + indexSource.includes("path: `/ccdiProject/detail/${data.projectId}`"), + "项目创建成功后应跳转到新项目详情页" +); + +assert( + indexSource.includes('query: { tab: "upload" }') || + indexSource.includes("query: { tab: 'upload' }"), + "项目创建成功后应显式激活上传数据页签" +); + +assert( + !/handleSubmitProject\(data\)\s*\{[\s\S]*?this\.getList\(\)\s*\/\/ 刷新列表[\s\S]*?\}/.test(indexSource), + "项目创建成功后不应只刷新列表" +); + +console.log("project-create-upload-jump test passed"); diff --git a/ruoyi-ui/tests/unit/project-tag-failed-status.test.js b/ruoyi-ui/tests/unit/project-tag-failed-status.test.js new file mode 100644 index 00000000..473557e0 --- /dev/null +++ b/ruoyi-ui/tests/unit/project-tag-failed-status.test.js @@ -0,0 +1,78 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +function readSource(relativePath) { + return fs.readFileSync(path.resolve(__dirname, "../../", relativePath), "utf8"); +} + +const detailSource = readSource("src/views/ccdiProject/detail.vue"); +const tableSource = readSource("src/views/ccdiProject/components/ProjectTable.vue"); +const searchBarSource = readSource("src/views/ccdiProject/components/SearchBar.vue"); +const indexSource = readSource("src/views/ccdiProject/index.vue"); +const uploadSource = readSource("src/views/ccdiProject/components/detail/UploadData.vue"); +const paramSource = readSource("src/views/ccdiProject/components/detail/ParamConfig.vue"); + +assert( + detailSource.includes("latestTagTaskErrorMessage") && + detailSource.includes("latestTagTaskEndTime"), + "项目详情接口字段应承载最近失败任务的错误信息和结束时间" +); + +assert( + detailSource.includes("tagFailureDialogVisible") && + detailSource.includes("项目打标失败") && + detailSource.includes("查看完整错误"), + "项目详情页应展示打标失败提示并提供完整错误弹窗" +); + +assert( + /isProjectTagFailed\(\)\s*\{\s*return String\(this\.projectInfo\.projectStatus\) === "4";\s*\}/.test( + detailSource + ), + "项目详情页应显式识别 4-打标失败状态" +); + +assert( + /4:\s*"danger"/.test(detailSource) && /4:\s*"打标失败"/.test(detailSource), + "项目详情页状态映射应支持 4-打标失败" +); + +assert( + /String\(this\.projectInfo\.projectStatus\)\s*!==\s*"3"/.test(detailSource), + "项目状态轮询应在状态变为打标失败后停止" +); + +assert( + tableSource.includes("['0', '3', '4'].includes(scope.row.status)"), + "项目列表中打标失败项目应只开放进入项目入口" +); + +assert( + !tableSource.includes("latestTagTaskErrorMessage"), + "项目列表不应展示完整打标失败错误" +); + +assert( + searchBarSource.includes("{ label: '打标失败', value: '4', count: 0 }"), + "项目筛选栏应提供打标失败筛选项" +); + +assert( + indexSource.includes("'4': counts.status4 || 0"), + "项目首页应接收 status4 统计" +); + +assert( + uploadSource.includes('return ["0", "3", "4"].includes(String(this.projectInfo.projectStatus));'), + "上传数据页应将打标失败按进行中口径禁用报告入口" +); + +assert( + !uploadSource.includes("isProjectTagFailed") && + !paramSource.includes("isProjectTagFailed") && + !paramSource.includes('projectStatus) === "4"'), + "上传和参数配置页不应把打标失败作为只读锁定状态" +); + +console.log("project-tag-failed-status test passed"); diff --git a/ruoyi-ui/tests/unit/purchase-transaction-enterprise-detail-ui.test.js b/ruoyi-ui/tests/unit/purchase-transaction-enterprise-detail-ui.test.js new file mode 100644 index 00000000..b6104703 --- /dev/null +++ b/ruoyi-ui/tests/unit/purchase-transaction-enterprise-detail-ui.test.js @@ -0,0 +1,51 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const componentPath = path.resolve( + __dirname, + "../../src/views/ccdiPurchaseTransaction/index.vue" +); +const source = fs.readFileSync(componentPath, "utf8"); + +[ + 'from "@/api/ccdiEnterpriseBaseInfo"', + 'from "@/api/ccdiEnum"', + "enterpriseDetailOpen", + "enterpriseDetailLoading", + "enterpriseDetailData", + "handleSupplierEnterpriseDetail(row)", + "resetEnterpriseDetail()", + "暂无企业信息" +].forEach((token) => { + assert(source.includes(token), `招投标供应商企业详情缺少关键结构: ${token}`); +}); + +[ + 'label="操作"', + ">详情", + "企业信息详情", + "统一社会信用代码", + "企业名称", + "企业类型", + "企业性质", + "行业分类", + "所属行业", + "法定代表人", + "风险等级", + "企业来源", + "数据来源", + "股东5" +].forEach((token) => { + assert(source.includes(token), `招投标供应商企业详情模板缺少关键结构: ${token}`); +}); + +[ + "v-hasPermi=\\\"['ccdi:enterpriseBaseInfo:query']\\\"", + "v-hasPermi=\"['ccdi:enterpriseBaseInfo:query']\"", + "ccdi:enterpriseBaseInfo:query" +].forEach((token) => { + assert(!source.includes(token), `本次不应新增实体库权限显隐控制: ${token}`); +}); + +console.log("purchase-transaction-enterprise-detail-ui test passed");