This commit is contained in:
wkc
2026-07-02 16:48:17 +08:00
parent 979ed9669f
commit 87fb6443e6
27 changed files with 2167 additions and 0 deletions

View File

@@ -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<EasyExcelUtil> 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<EasyExcelUtil> 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<EasyExcelUtil> 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<EasyExcelUtil> 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));
}
}
}

View File

@@ -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());
}
}

View File

@@ -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<EasyExcelUtil> 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<EasyExcelUtil> 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<EasyExcelUtil> 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<EasyExcelUtil> 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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<String, Object> redisTemplate;
@Mock
private HashOperations<String, Object, Object> hashOperations;
@Mock
private ValueOperations<String, Object> 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<Object> 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<Object> 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<Map<String, Object>> 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;
}
}

View File

@@ -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<String, Object> 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<CcdiBizIntermediary> 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");
}
}

View File

@@ -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);
}
}

View File

@@ -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<CcdiBaseStaffExcel> 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<PlainExcel> 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;
}
}
}

View File

@@ -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<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass(Map.class);
ArgumentCaptor<Map<String, String>> 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));
}
}

View File

@@ -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<HttpEntity> captor = ArgumentCaptor.forClass(HttpEntity.class);
when(restTemplate.postForEntity(eq("http://lsfx/upload"), captor.capture(), eq(String.class)))
.thenReturn(ResponseEntity.ok("ok"));
Map<String, Object> 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<String, Object> body = (MultiValueMap<String, Object>) captor.getValue().getBody();
Object filePart = body.getFirst("files");
Resource resource = assertInstanceOf(Resource.class, filePart);
assertEquals("银行流水A.xlsx", resource.getFilename());
}
}

View File

@@ -0,0 +1 @@
mock-maker-subclass

View File

@@ -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);
}
}
}

View File

@@ -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"));
}
}

View File

@@ -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("<update id=\"markProjectDeleted\">"), xml);
assertTrue(xml.contains("status = '5'"), xml);
assertTrue(xml.contains("del_flag = '2'"), xml);
assertTrue(xml.contains("<update id=\"restoreDeletedProject\">"), 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 = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start);
assertTrue(startIndex >= 0, "missing select: " + selectId);
int endIndex = xml.indexOf("</select>", startIndex);
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
return xml.substring(startIndex, endIndex);
}
}

View File

@@ -0,0 +1 @@
mock-maker-subclass

View File

@@ -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. 结论
- 两类导入功能的模板下载、模板改写、页面上传、异步轮询、失败记录弹窗均可正常工作。
- 中介和亲属信息导入已覆盖:
- 成功导入
- 本人行关联字段错误
- 亲属缺少关联本人
- 姓名为空
- 人员子类型为空
- 证件号非法
- 文件内本人重复
- 关联本人不存在
- 文件内亲属重复
- 库内本人重复
- 库内亲属重复
- 中介实体关联关系导入已覆盖:
- 成功导入
- 中介本人为空
- 中介本人证件号非法
- 中介本人不存在
- 统一社会信用代码为空
- 统一社会信用代码不存在
- 关联人职务超长
- 备注超长
- 文件内关系重复
- 库内关系重复

View File

@@ -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` 拉起的后端进程

View File

@@ -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` 进程

View File

@@ -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 提交范围。

View File

@@ -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 提交范围。

View File

@@ -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 提交范围。

View File

@@ -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|@"));
}
}

View File

@@ -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();
}
}

View File

@@ -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");

View File

@@ -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");

View File

@@ -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="操作"',
">详情</el-button>",
"企业信息详情",
"统一社会信用代码",
"企业名称",
"企业类型",
"企业性质",
"行业分类",
"所属行业",
"法定代表人",
"风险等级",
"企业来源",
"数据来源",
"股东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");