调整实体库管理数据来源维护规则

This commit is contained in:
wkc
2026-04-23 17:31:56 +08:00
parent d444eafd5f
commit b7d020c0b2
12 changed files with 630 additions and 125 deletions

View File

@@ -89,7 +89,6 @@ public class CcdiEnterpriseBaseInfoAddDTO implements Serializable {
private String shareholder5; private String shareholder5;
@Schema(description = "经营状态") @Schema(description = "经营状态")
@NotBlank(message = "经营状态不能为空")
@Size(max = 50, message = "经营状态长度不能超过50个字符") @Size(max = 50, message = "经营状态长度不能超过50个字符")
private String status; private String status;
@@ -102,6 +101,5 @@ public class CcdiEnterpriseBaseInfoAddDTO implements Serializable {
private String entSource; private String entSource;
@Schema(description = "数据来源") @Schema(description = "数据来源")
@NotBlank(message = "数据来源不能为空")
private String dataSource; private String dataSource;
} }

View File

@@ -89,7 +89,6 @@ public class CcdiEnterpriseBaseInfoEditDTO implements Serializable {
private String shareholder5; private String shareholder5;
@Schema(description = "经营状态") @Schema(description = "经营状态")
@NotBlank(message = "经营状态不能为空")
@Size(max = 50, message = "经营状态长度不能超过50个字符") @Size(max = 50, message = "经营状态长度不能超过50个字符")
private String status; private String status;

View File

@@ -88,7 +88,7 @@ public class CcdiEnterpriseBaseInfoExcel implements Serializable {
@ColumnWidth(18) @ColumnWidth(18)
private String shareholder5; private String shareholder5;
@ExcelProperty(value = "经营状态*", index = 16) @ExcelProperty(value = "经营状态", index = 16)
@ColumnWidth(16) @ColumnWidth(16)
private String status; private String status;
@@ -99,8 +99,4 @@ public class CcdiEnterpriseBaseInfoExcel implements Serializable {
@ExcelProperty(value = "企业来源*", index = 18) @ExcelProperty(value = "企业来源*", index = 18)
@ColumnWidth(18) @ColumnWidth(18)
private String entSource; private String entSource;
@ExcelProperty(value = "数据来源*", index = 19)
@ColumnWidth(18)
private String dataSource;
} }

View File

@@ -131,10 +131,6 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
if (!excel.getSocialCreditCode().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) { if (!excel.getSocialCreditCode().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) {
throw new RuntimeException("统一社会信用代码格式不正确"); throw new RuntimeException("统一社会信用代码格式不正确");
} }
if (StringUtils.isEmpty(excel.getStatus())) {
throw new RuntimeException("经营状态不能为空");
}
String riskLevel = EnterpriseRiskLevel.resolveCode(StringUtils.trim(excel.getRiskLevel())); String riskLevel = EnterpriseRiskLevel.resolveCode(StringUtils.trim(excel.getRiskLevel()));
if (riskLevel == null) { if (riskLevel == null) {
throw new RuntimeException("风险等级不在允许范围内"); throw new RuntimeException("风险等级不在允许范围内");
@@ -143,10 +139,6 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
if (entSource == null) { if (entSource == null) {
throw new RuntimeException("企业来源不在允许范围内"); throw new RuntimeException("企业来源不在允许范围内");
} }
String dataSource = resolveDataSourceCode(StringUtils.trim(excel.getDataSource()));
if (dataSource == null) {
throw new RuntimeException("数据来源不在允许范围内");
}
if (existingCreditCodes.contains(excel.getSocialCreditCode())) { if (existingCreditCodes.contains(excel.getSocialCreditCode())) {
throw new RuntimeException(String.format("统一社会信用代码[%s]已存在,请勿重复导入", excel.getSocialCreditCode())); throw new RuntimeException(String.format("统一社会信用代码[%s]已存在,请勿重复导入", excel.getSocialCreditCode()));
@@ -159,8 +151,8 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
BeanUtils.copyProperties(excel, entity); BeanUtils.copyProperties(excel, entity);
entity.setRiskLevel(riskLevel); entity.setRiskLevel(riskLevel);
entity.setEntSource(entSource); entity.setEntSource(entSource);
entity.setDataSource(dataSource); entity.setDataSource(DataSource.IMPORT.getCode());
entity.setStatus(StringUtils.trim(excel.getStatus())); entity.setStatus(trimToNull(excel.getStatus()));
entity.setCreatedBy(userName); entity.setCreatedBy(userName);
entity.setUpdatedBy(userName); entity.setUpdatedBy(userName);
return entity; return entity;
@@ -206,15 +198,6 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
redisTemplate.opsForHash().putAll(buildStatusKey(taskId), statusData); redisTemplate.opsForHash().putAll(buildStatusKey(taskId), statusData);
} }
private String resolveDataSourceCode(String value) {
for (DataSource source : DataSource.values()) {
if (source.getCode().equals(value) || source.getDesc().equals(value)) {
return source.getCode();
}
}
return null;
}
private String buildStatusKey(String taskId) { private String buildStatusKey(String taskId) {
return "import:enterpriseBaseInfo:" + taskId; return "import:enterpriseBaseInfo:" + taskId;
} }
@@ -222,4 +205,11 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
private String buildFailuresKey(String taskId) { private String buildFailuresKey(String taskId) {
return "import:enterpriseBaseInfo:" + taskId + ":failures"; return "import:enterpriseBaseInfo:" + taskId + ":failures";
} }
private String trimToNull(String value) {
if (StringUtils.isEmpty(value)) {
return null;
}
return value.trim();
}
} }

View File

@@ -85,10 +85,12 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
if (enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode()) != null) { if (enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode()) != null) {
throw new RuntimeException("该统一社会信用代码已存在"); throw new RuntimeException("该统一社会信用代码已存在");
} }
validateEnumFields(addDTO.getStatus(), addDTO.getRiskLevel(), addDTO.getEntSource(), addDTO.getDataSource()); validateRiskLevelAndEnterpriseSource(addDTO.getRiskLevel(), addDTO.getEntSource());
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo(); CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
BeanUtils.copyProperties(addDTO, entity); BeanUtils.copyProperties(addDTO, entity);
entity.setStatus(trimToNull(addDTO.getStatus()));
entity.setDataSource(DataSource.MANUAL.getCode());
return enterpriseBaseInfoMapper.insert(entity); return enterpriseBaseInfoMapper.insert(entity);
} }
@@ -103,6 +105,8 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo(); CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
BeanUtils.copyProperties(editDTO, entity); BeanUtils.copyProperties(editDTO, entity);
entity.setStatus(trimToNull(editDTO.getStatus()));
entity.setDataSource(existing.getDataSource());
return enterpriseBaseInfoMapper.updateById(entity); return enterpriseBaseInfoMapper.updateById(entity);
} }
@@ -176,18 +180,22 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
} }
private void validateEnumFields(String status, String riskLevel, String entSource, String dataSource) { private void validateEnumFields(String status, String riskLevel, String entSource, String dataSource) {
if (StringUtils.isEmpty(status)) { validateRiskLevelAndEnterpriseSource(riskLevel, entSource);
throw new RuntimeException("经营状态不能为空"); if (StringUtils.isNotEmpty(status) && StringUtils.trim(status).length() > 50) {
throw new RuntimeException("经营状态长度不能超过50个字符");
} }
if (!containsDataSource(dataSource)) {
throw new RuntimeException("数据来源不在允许范围内");
}
}
private void validateRiskLevelAndEnterpriseSource(String riskLevel, String entSource) {
if (!EnterpriseRiskLevel.contains(riskLevel)) { if (!EnterpriseRiskLevel.contains(riskLevel)) {
throw new RuntimeException("风险等级不在允许范围内"); throw new RuntimeException("风险等级不在允许范围内");
} }
if (!EnterpriseSource.contains(entSource)) { if (!EnterpriseSource.contains(entSource)) {
throw new RuntimeException("企业来源不在允许范围内"); throw new RuntimeException("企业来源不在允许范围内");
} }
if (!containsDataSource(dataSource)) {
throw new RuntimeException("数据来源不在允许范围内");
}
} }
private boolean containsDataSource(String code) { private boolean containsDataSource(String code) {
@@ -199,6 +207,13 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
return false; return false;
} }
private String trimToNull(String value) {
if (StringUtils.isEmpty(value)) {
return null;
}
return value.trim();
}
private void validateDeleteRelations(String socialCreditCode) { private void validateDeleteRelations(String socialCreditCode) {
StringJoiner relationTypes = new StringJoiner(""); StringJoiner relationTypes = new StringJoiner("");
if (staffEnterpriseRelationMapper.selectCount(new LambdaQueryWrapper<CcdiStaffEnterpriseRelation>() if (staffEnterpriseRelationMapper.selectCount(new LambdaQueryWrapper<CcdiStaffEnterpriseRelation>()

View File

@@ -0,0 +1,77 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
import com.ruoyi.info.collection.service.impl.CcdiEnterpriseBaseInfoImportServiceImpl;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CcdiEnterpriseBaseInfoImportServiceImplTest {
private final CcdiEnterpriseBaseInfoImportServiceImpl service = new CcdiEnterpriseBaseInfoImportServiceImpl();
@Test
void validateAndBuildEntity_shouldRejectWhenDatabaseAlreadyContainsCreditCode() {
CcdiEnterpriseBaseInfoExcel excel = buildExcel();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateAndBuildEntity(excel, Set.of("91310000123456789A"), new HashSet<>(), "admin"));
assertEquals("统一社会信用代码[91310000123456789A]已存在,请勿重复导入", exception.getMessage());
}
@Test
void validateAndBuildEntity_shouldRejectWhenExcelContainsDuplicateCreditCode() {
CcdiEnterpriseBaseInfoExcel excel = buildExcel();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateAndBuildEntity(excel, Set.of(), new HashSet<>(Set.of("91310000123456789A")), "admin"));
assertEquals("统一社会信用代码[91310000123456789A]在导入文件中重复,已跳过此条记录", exception.getMessage());
}
@Test
void validateAndBuildEntity_shouldNormalizeEnumTextToCode() {
CcdiEnterpriseBaseInfoExcel excel = buildExcel();
excel.setRiskLevel("高风险");
excel.setEntSource("一般企业");
CcdiEnterpriseBaseInfo entity = service.validateAndBuildEntity(excel, Set.of(), new HashSet<>(), "admin");
assertEquals("1", entity.getRiskLevel());
assertEquals("GENERAL", entity.getEntSource());
assertEquals("IMPORT", entity.getDataSource());
assertEquals("admin", entity.getCreatedBy());
}
@Test
void validateAndBuildEntity_shouldAllowBlankStatus() {
CcdiEnterpriseBaseInfoExcel excel = buildExcel();
excel.setStatus(null);
CcdiEnterpriseBaseInfo entity = service.validateAndBuildEntity(excel, Set.of(), new HashSet<>(), "admin");
assertNull(entity.getStatus());
assertEquals("IMPORT", entity.getDataSource());
}
private CcdiEnterpriseBaseInfoExcel buildExcel() {
CcdiEnterpriseBaseInfoExcel excel = new CcdiEnterpriseBaseInfoExcel();
excel.setSocialCreditCode("91310000123456789A");
excel.setEnterpriseName("测试企业");
excel.setEnterpriseType("有限责任公司");
excel.setEnterpriseNature("民营企业");
excel.setIndustryClass("制造业");
excel.setIndustryName("电子设备");
excel.setStatus("存续");
excel.setRiskLevel("1");
excel.setEntSource("GENERAL");
return excel;
}
}

View File

@@ -0,0 +1,204 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
import com.ruoyi.info.collection.domain.vo.CcdiEnterpriseBaseInfoVO;
import com.ruoyi.info.collection.mapper.CcdiCustEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.impl.CcdiEnterpriseBaseInfoServiceImpl;
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.assertNotNull;
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.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiEnterpriseBaseInfoServiceImplTest {
@InjectMocks
private CcdiEnterpriseBaseInfoServiceImpl service;
@Mock
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
@Mock
private CcdiStaffEnterpriseRelationMapper staffEnterpriseRelationMapper;
@Mock
private CcdiCustEnterpriseRelationMapper custEnterpriseRelationMapper;
@Mock
private CcdiIntermediaryEnterpriseRelationMapper intermediaryEnterpriseRelationMapper;
@Mock
private ICcdiEnterpriseBaseInfoImportService enterpriseBaseInfoImportService;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Test
void insertEnterpriseBaseInfo_shouldPersistWhenSocialCreditCodeIsUnique() {
CcdiEnterpriseBaseInfoAddDTO addDTO = buildAddDto();
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
when(enterpriseBaseInfoMapper.insert(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
int result = service.insertEnterpriseBaseInfo(addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
verify(enterpriseBaseInfoMapper).insert(captor.capture());
assertEquals("测试企业", captor.getValue().getEnterpriseName());
assertEquals("GENERAL", captor.getValue().getEntSource());
}
@Test
void insertEnterpriseBaseInfo_shouldSetManualDataSourceAndAllowBlankStatus() {
CcdiEnterpriseBaseInfoAddDTO addDTO = buildAddDto();
addDTO.setStatus(null);
addDTO.setDataSource("API");
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
when(enterpriseBaseInfoMapper.insert(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
int result = service.insertEnterpriseBaseInfo(addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
verify(enterpriseBaseInfoMapper).insert(captor.capture());
assertEquals("MANUAL", captor.getValue().getDataSource());
assertNull(captor.getValue().getStatus());
}
@Test
void insertEnterpriseBaseInfo_shouldRejectInvalidRiskLevel() {
CcdiEnterpriseBaseInfoAddDTO addDTO = buildAddDto();
addDTO.setRiskLevel("9");
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.insertEnterpriseBaseInfo(addDTO));
assertEquals("风险等级不在允许范围内", exception.getMessage());
}
@Test
void updateEnterpriseBaseInfo_shouldRejectWhenRecordMissing() {
CcdiEnterpriseBaseInfoEditDTO editDTO = buildEditDto();
when(enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode())).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.updateEnterpriseBaseInfo(editDTO));
assertEquals("实体库记录不存在", exception.getMessage());
}
@Test
void updateEnterpriseBaseInfo_shouldKeepExistingDataSource() {
CcdiEnterpriseBaseInfoEditDTO editDTO = buildEditDto();
editDTO.setDataSource("API");
CcdiEnterpriseBaseInfo existing = new CcdiEnterpriseBaseInfo();
existing.setSocialCreditCode(editDTO.getSocialCreditCode());
existing.setDataSource("MANUAL");
when(enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode())).thenReturn(existing);
when(enterpriseBaseInfoMapper.updateById(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
int result = service.updateEnterpriseBaseInfo(editDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
verify(enterpriseBaseInfoMapper).updateById(captor.capture());
assertEquals("MANUAL", captor.getValue().getDataSource());
}
@Test
void selectEnterpriseBaseInfoById_shouldConvertEntityToVo() {
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
entity.setSocialCreditCode("91310000123456789A");
entity.setEnterpriseName("测试企业");
entity.setRiskLevel("1");
entity.setEntSource("GENERAL");
when(enterpriseBaseInfoMapper.selectById("91310000123456789A")).thenReturn(entity);
CcdiEnterpriseBaseInfoVO vo = service.selectEnterpriseBaseInfoById("91310000123456789A");
assertNotNull(vo);
assertEquals("测试企业", vo.getEnterpriseName());
assertEquals("1", vo.getRiskLevel());
}
@Test
void deleteEnterpriseBaseInfoByIds_shouldDeleteInBatch() {
when(staffEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
when(custEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
when(intermediaryEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
when(enterpriseBaseInfoMapper.deleteBatchIds(java.util.List.of("91310000123456789A", "91310000123456789B")))
.thenReturn(2);
int result = service.deleteEnterpriseBaseInfoByIds(new String[]{"91310000123456789A", "91310000123456789B"});
assertEquals(2, result);
verify(enterpriseBaseInfoMapper).deleteBatchIds(java.util.List.of("91310000123456789A", "91310000123456789B"));
}
@Test
void deleteEnterpriseBaseInfoByIds_shouldRejectWhenStaffRelationExists() {
when(staffEnterpriseRelationMapper.selectCount(any())).thenReturn(1L);
when(custEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
when(intermediaryEnterpriseRelationMapper.selectCount(any())).thenReturn(0L);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.deleteEnterpriseBaseInfoByIds(new String[]{"91310000123456789A"}));
assertEquals("统一社会信用代码[91310000123456789A]已关联员工,删除失败", exception.getMessage());
}
@Test
void deleteEnterpriseBaseInfoByIds_shouldRejectWhenMultipleRelationsExist() {
when(staffEnterpriseRelationMapper.selectCount(any())).thenReturn(1L);
when(custEnterpriseRelationMapper.selectCount(any())).thenReturn(1L);
when(intermediaryEnterpriseRelationMapper.selectCount(any())).thenReturn(1L);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.deleteEnterpriseBaseInfoByIds(new String[]{"91310000123456789A"}));
assertEquals("统一社会信用代码[91310000123456789A]已关联员工、信贷客户、中介,删除失败", exception.getMessage());
}
private CcdiEnterpriseBaseInfoAddDTO buildAddDto() {
CcdiEnterpriseBaseInfoAddDTO dto = new CcdiEnterpriseBaseInfoAddDTO();
dto.setSocialCreditCode("91310000123456789A");
dto.setEnterpriseName("测试企业");
dto.setEnterpriseType("有限责任公司");
dto.setEnterpriseNature("民营企业");
dto.setIndustryClass("制造业");
dto.setIndustryName("电子设备");
dto.setStatus("存续");
dto.setRiskLevel("1");
dto.setEntSource("GENERAL");
dto.setDataSource("MANUAL");
return dto;
}
private CcdiEnterpriseBaseInfoEditDTO buildEditDto() {
CcdiEnterpriseBaseInfoEditDTO dto = new CcdiEnterpriseBaseInfoEditDTO();
dto.setSocialCreditCode("91310000123456789A");
dto.setEnterpriseName("测试企业");
dto.setStatus("存续");
dto.setRiskLevel("1");
dto.setEntSource("GENERAL");
dto.setDataSource("MANUAL");
return dto;
}
}

View File

@@ -4,11 +4,13 @@ import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.utils.DictUtils; import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel; import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel; import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel; import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel; import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel; import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataValidation; import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellRangeAddress;
@@ -129,6 +131,30 @@ class EasyExcelUtilTemplateTest {
} }
} }
@Test
void importTemplateWithDictDropdown_shouldUseUpdatedEnterpriseHeaders() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
try (MockedStatic<DictUtils> mocked = mockStatic(DictUtils.class)) {
mocked.when(() -> DictUtils.getDictCache("ccdi_entity_type"))
.thenReturn(List.of(buildDictData("有限责任公司")));
mocked.when(() -> DictUtils.getDictCache("ccdi_enterprise_nature"))
.thenReturn(List.of(buildDictData("民营企业")));
mocked.when(() -> DictUtils.getDictCache("ccdi_certificate_type"))
.thenReturn(List.of(buildDictData("居民身份证")));
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiEnterpriseBaseInfoExcel.class, "实体库管理");
}
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
Row headerRow = workbook.getSheetAt(0).getRow(0);
assertEquals("经营状态", headerRow.getCell(16).getStringCellValue());
assertEquals("风险等级*", headerRow.getCell(17).getStringCellValue());
assertEquals("企业来源*", headerRow.getCell(18).getStringCellValue());
assertEquals(19, headerRow.getLastCellNum());
}
}
private void assertTextColumn(Sheet sheet, int columnIndex) { private void assertTextColumn(Sheet sheet, int columnIndex) {
CellStyle style = sheet.getColumnStyle(columnIndex); CellStyle style = sheet.getColumnStyle(columnIndex);
assertNotNull(style, "文本列应设置默认样式"); assertNotNull(style, "文本列应设置默认样式");

View File

@@ -0,0 +1,50 @@
# 实体库管理新增弹窗后端实施计划
## 文档路径确认
- 后端实施计划保存路径:`docs/plans/backend/`
- 本文档文件名:`2026-04-23-enterprise-base-info-add-dialog-backend-implementation.md`
## 需求目标
- 实体库管理新增场景不再要求前端传入数据来源。
- 新增时由后端自动写入数据来源,人工新增统一落为 `MANUAL`
- 编辑时后端保留原有数据来源,不允许通过请求修改。
- 经营状态调整为非必填项。
- 导入模板同步去除数据来源输入列,并将经营状态改为非必填展示。
- 导入时由后端自动写入 `IMPORT`,不再依赖模板传值。
## 实施范围
- `CcdiEnterpriseBaseInfoAddDTO`
- `CcdiEnterpriseBaseInfoEditDTO`
- `CcdiEnterpriseBaseInfoServiceImpl`
- `CcdiEnterpriseBaseInfoImportServiceImpl`
- `CcdiEnterpriseBaseInfoExcel`
## 实施步骤
1. 去除新增 DTO 中 `status``dataSource` 的必填约束,编辑 DTO 中去除 `status` 的必填约束。
2. 调整新增服务逻辑,仅校验风险等级和企业来源,新增数据时后端固定写入 `MANUAL`
3. 调整编辑服务逻辑,更新时始终沿用数据库中的原始数据来源。
4. 调整状态字段赋值逻辑,将空白字符串统一收敛为 `null`,避免出现空串脏数据。
5. 调整导入服务逻辑,去掉对模板数据来源的解析和校验,导入时固定写入 `IMPORT`
6. 修改导入模板对象,移除数据来源列,并把“经营状态*”调整为“经营状态”。
7. 补充/更新单元测试,覆盖新增自动写入 `MANUAL`、编辑保留原数据来源、导入自动写入 `IMPORT`、经营状态非必填和模板表头变更。
## 验证方案
- 执行定向测试:
`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiEnterpriseBaseInfoServiceImplTest,CcdiEnterpriseBaseInfoImportServiceImplTest,EasyExcelUtilTemplateTest test`
- 通过真实后端接口验证:
- 新增不传 `dataSource`、不传 `status` 仍可成功
- 查询结果中 `dataSource=MANUAL`
- 删除测试数据成功
- 下载导入模板并核对首行表头:
- 包含 `经营状态`
- 不包含 `数据来源`
## 风险与注意事项
- 当前工作区存在其他未提交改动,本次仅处理实体库管理相关文件,不回退无关内容。
- 导入模板对象同时承担导入解析职责,本次变更后模板和导入字段保持一致。

View File

@@ -0,0 +1,41 @@
# 实体库管理新增弹窗前端实施计划
## 文档路径确认
- 前端实施计划保存路径:`docs/plans/frontend/`
- 本文档文件名:`2026-04-23-enterprise-base-info-add-dialog-frontend-implementation.md`
## 需求目标
- 实体库管理新增弹窗隐藏“数据来源”字段。
- 经营状态改为非必填。
- 编辑弹窗仍保留数据来源展示,但不允许修改。
- 导入入口沿用现有交互,但模板下载出的字段定义要与后端保持一致。
## 实施范围
- `ruoyi-ui/src/views/ccdiEnterpriseBaseInfo/index.vue`
## 实施步骤
1. 在表单层按 `isAdd` 区分新增与编辑模式,新增时隐藏“数据来源”表单项。
2. 去除经营状态前端必填校验。
3. 编辑态将数据来源改为只读展示,避免用户误改。
4. 调整数据来源校验逻辑,仅用于保持表单已有值,不提供编辑能力。
5. 新增提交时从请求载荷中删除 `dataSource` 字段,避免旧表单状态误传。
6. 保留列表、详情、编辑页中的数据来源展示,避免影响已有查看能力。
## 验证方案
- 使用 `nvm use` 切换到仓库要求的 Node 版本后启动前端:
`npm run dev`
- 浏览器进入 `实体库管理` 页面,核对:
- 新增弹窗没有“数据来源”字段
- 经营状态没有必填星号
- 风险等级、企业来源仍为必填
- 导入弹窗可正常打开
## 风险与注意事项
- 当前页面文件存在其他在途改动,本次仅追加最小范围修改,不覆盖无关调整。
- 新增与编辑共用同一个表单对象,需要通过条件渲染和条件校验避免互相影响。

View File

@@ -0,0 +1,80 @@
# 实体库管理新增弹窗与导入模板调整实施记录
## 文档路径确认
- 实施记录保存路径:`docs/reports/implementation/`
- 本文档文件名:`2026-04-23-enterprise-base-info-add-dialog-implementation.md`
## 本次修改内容
- 新增弹窗隐藏“数据来源”字段。
- 新增时后端自动写入 `MANUAL`,不再依赖前端传值。
- 编辑时数据来源改为只读展示,后端更新时保留原值不变。
- 经营状态改为非必填,空值统一按 `null` 落库。
- 导入模板移除“数据来源”列,并将“经营状态*”改为“经营状态”。
- 导入时后端自动写入 `IMPORT`
## 影响范围
- 后端:
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiEnterpriseBaseInfoAddDTO.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiEnterpriseBaseInfoEditDTO.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiEnterpriseBaseInfoExcel.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiEnterpriseBaseInfoServiceImpl.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiEnterpriseBaseInfoImportServiceImpl.java`
- 前端:
- `ruoyi-ui/src/views/ccdiEnterpriseBaseInfo/index.vue`
- 测试:
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiEnterpriseBaseInfoServiceImplTest.java`
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiEnterpriseBaseInfoImportServiceImplTest.java`
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java`
## 验证结果
### 1. 定向单元测试
- 命令:
`mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiEnterpriseBaseInfoServiceImplTest,CcdiEnterpriseBaseInfoImportServiceImplTest,EasyExcelUtilTemplateTest test`
- 结果:
17 个定向测试全部通过。
### 补充调整
- 根据追加要求,编辑弹窗中的数据来源已改为不可修改展示。
- 后端更新接口忽略请求里的 `dataSource` 变更,统一沿用数据库原值。
### 2. 真实后端接口验证
- 使用 `/login/test` 获取 token 后,调用新增接口仅传:
- `socialCreditCode`
- `enterpriseName`
- `riskLevel`
- `entSource`
- 结果:
- 新增成功
- 查询结果中 `status=null`
- 查询结果中 `dataSource=MANUAL`
- 删除测试数据成功
### 3. 导入模板实际下载校验
- 通过真实接口下载模板文件到 `output/spreadsheet/enterprise-base-info-template-20260423.xlsx`
- 核对首行表头结果:
- 包含 `经营状态`
- 包含 `风险等级*`
- 包含 `企业来源*`
- 不包含 `数据来源`
### 4. 浏览器实际页面验证
- 前端按仓库要求执行 `nvm use`,确认使用 `Node v14.21.3`
- 启动 `ruoyi-ui` 开发服务并用 Playwright 打开真实页面 `实体库管理`
- 实际观察结果:
- 新增弹窗中“数据来源”字段已隐藏
- 经营状态不再显示为必填项
- 导入入口可正常打开
## 过程说明
- 验证时发现 `62318` 初始运行的是旧后端进程,仍返回“经营状态不能为空”,随后已按规范使用 `bin/restart_java_backend.sh restart` 重启后端并完成回归验证。
- 浏览器内尝试直接提交测试数据时页面没有明确反馈,因此补充了真实接口新增/查询/删除验证来完成链路闭环,并已清理测试数据。

View File

@@ -1,28 +1,34 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true" v-show="showSearch" label-width="110px"> <el-form ref="queryForm" :model="queryParams" size="small" v-show="showSearch" label-width="110px" class="query-form">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="企业名称" prop="enterpriseName"> <el-form-item label="企业名称" prop="enterpriseName">
<el-input <el-input
v-model="queryParams.enterpriseName" v-model="queryParams.enterpriseName"
placeholder="请输入企业名称" placeholder="请输入企业名称"
clearable clearable
style="width: 240px" style="width: 100%"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="统一社会信用代码" prop="socialCreditCode"> <el-form-item label="统一社会信用代码" prop="socialCreditCode">
<el-input <el-input
v-model="queryParams.socialCreditCode" v-model="queryParams.socialCreditCode"
placeholder="请输入统一社会信用代码" placeholder="请输入统一社会信用代码"
clearable clearable
maxlength="18" maxlength="18"
style="width: 240px" style="width: 100%"
@input="queryParams.socialCreditCode = normalizeUpperCode(queryParams.socialCreditCode)" @input="queryParams.socialCreditCode = normalizeUpperCode(queryParams.socialCreditCode)"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="企业类型" prop="enterpriseType"> <el-form-item label="企业类型" prop="enterpriseType">
<el-select v-model="queryParams.enterpriseType" placeholder="请选择企业类型" clearable style="width: 240px"> <el-select v-model="queryParams.enterpriseType" placeholder="请选择企业类型" clearable style="width: 100%">
<el-option <el-option
v-for="item in corpTypeOptions" v-for="item in corpTypeOptions"
:key="item.value" :key="item.value"
@@ -31,8 +37,10 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="企业性质" prop="enterpriseNature"> <el-form-item label="企业性质" prop="enterpriseNature">
<el-select v-model="queryParams.enterpriseNature" placeholder="请选择企业性质" clearable style="width: 240px"> <el-select v-model="queryParams.enterpriseNature" placeholder="请选择企业性质" clearable style="width: 100%">
<el-option <el-option
v-for="item in corpNatureOptions" v-for="item in corpNatureOptions"
:key="item.value" :key="item.value"
@@ -41,26 +49,32 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="行业分类" prop="industryClass"> <el-form-item label="行业分类" prop="industryClass">
<el-input <el-input
v-model="queryParams.industryClass" v-model="queryParams.industryClass"
placeholder="请输入行业分类" placeholder="请输入行业分类"
clearable clearable
style="width: 240px" style="width: 100%"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="经营状态" prop="status"> <el-form-item label="经营状态" prop="status">
<el-input <el-input
v-model="queryParams.status" v-model="queryParams.status"
placeholder="请输入经营状态" placeholder="请输入经营状态"
clearable clearable
style="width: 240px" style="width: 100%"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="风险等级" prop="riskLevel"> <el-form-item label="风险等级" prop="riskLevel">
<el-select v-model="queryParams.riskLevel" placeholder="请选择风险等级" clearable style="width: 240px"> <el-select v-model="queryParams.riskLevel" placeholder="请选择风险等级" clearable style="width: 100%">
<el-option <el-option
v-for="item in riskLevelOptions" v-for="item in riskLevelOptions"
:key="item.value" :key="item.value"
@@ -69,8 +83,10 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="企业来源" prop="entSource"> <el-form-item label="企业来源" prop="entSource">
<el-select v-model="queryParams.entSource" placeholder="请选择企业来源" clearable style="width: 240px"> <el-select v-model="queryParams.entSource" placeholder="请选择企业来源" clearable style="width: 100%">
<el-option <el-option
v-for="item in enterpriseSourceOptions" v-for="item in enterpriseSourceOptions"
:key="item.value" :key="item.value"
@@ -79,6 +95,8 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
@@ -336,16 +354,9 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12" v-if="!isAdd">
<el-form-item label="数据来源" prop="dataSource"> <el-form-item label="数据来源" prop="dataSource">
<el-select v-model="form.dataSource" placeholder="请选择数据来源" style="width: 100%"> <el-input :value="formatDataSource(form.dataSource) || '-'" disabled />
<el-option
v-for="item in dataSourceOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -515,6 +526,13 @@ const socialCreditCodePattern = /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]
export default { export default {
name: "EnterpriseBaseInfo", name: "EnterpriseBaseInfo",
data() { data() {
const validateDataSource = (rule, value, callback) => {
if (this.isAdd || value) {
callback();
return;
}
callback(new Error("请选择数据来源"));
};
return { return {
loading: true, loading: true,
ids: [], ids: [],
@@ -556,9 +574,6 @@ export default {
{ required: true, message: "企业名称不能为空", trigger: "blur" }, { required: true, message: "企业名称不能为空", trigger: "blur" },
{ max: 200, message: "企业名称长度不能超过200个字符", trigger: "blur" } { max: 200, message: "企业名称长度不能超过200个字符", trigger: "blur" }
], ],
status: [
{ required: true, message: "经营状态不能为空", trigger: "blur" }
],
riskLevel: [ riskLevel: [
{ required: true, message: "请选择风险等级", trigger: "change" } { required: true, message: "请选择风险等级", trigger: "change" }
], ],
@@ -566,7 +581,7 @@ export default {
{ required: true, message: "请选择企业来源", trigger: "change" } { required: true, message: "请选择企业来源", trigger: "change" }
], ],
dataSource: [ dataSource: [
{ required: true, message: "请选择数据来源", trigger: "change" } { validator: validateDataSource, trigger: "change" }
] ]
}, },
upload: { upload: {
@@ -711,6 +726,7 @@ export default {
} }
var payload = this.normalizePayload(this.form); var payload = this.normalizePayload(this.form);
if (this.isAdd) { if (this.isAdd) {
delete payload.dataSource;
addEnterpriseBaseInfo(payload).then(() => { addEnterpriseBaseInfo(payload).then(() => {
this.$modal.msgSuccess("新增成功"); this.$modal.msgSuccess("新增成功");
this.open = false; this.open = false;
@@ -973,6 +989,19 @@ export default {
</script> </script>
<style scoped> <style scoped>
.query-form ::v-deep .el-row {
display: flex;
flex-wrap: wrap;
}
.query-form ::v-deep .el-col {
float: none;
}
.query-form ::v-deep .el-form-item {
margin-right: 0;
}
.el-upload__tip { .el-upload__tip {
line-height: 1.6; line-height: 1.6;
} }