feat: 接入新华社工商信息同步

This commit is contained in:
wkc
2026-07-29 17:56:27 +08:00
parent 2bcba71259
commit 5d004a66e8
72 changed files with 2610 additions and 59 deletions

View File

@@ -57,12 +57,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ccdi-lsfx</artifactId>
<version>3.9.1</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@@ -0,0 +1,23 @@
package com.ruoyi.info.collection.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/** 工商信息同步专用线程池。 */
@Configuration
public class EnterpriseProfileExecutorConfig {
@Bean("enterpriseProfileExecutor")
public Executor enterpriseProfileExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("enterprise-profile-");
executor.initialize();
return executor;
}
}

View File

@@ -8,6 +8,7 @@ import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoQueryDTO;
@@ -18,6 +19,8 @@ import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoImportService;
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoService;
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -56,6 +59,9 @@ public class CcdiEnterpriseBaseInfoController extends BaseController {
@Resource
private ICcdiEnterpriseBaseInfoImportService enterpriseBaseInfoImportService;
@Resource
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
@Operation(summary = "查询实体库列表")
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:list')")
@GetMapping("/list")
@@ -73,12 +79,36 @@ public class CcdiEnterpriseBaseInfoController extends BaseController {
return success(enterpriseBaseInfoService.selectEnterpriseBaseInfoById(socialCreditCode));
}
@Operation(summary = "重新查询实体工商信息")
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:refresh')")
@PostMapping("/enterprise-profile/refresh/{socialCreditCode}")
public AjaxResult refreshEnterpriseProfile(@PathVariable String socialCreditCode) {
enterpriseEntitySyncService.refresh(CallerContext.from(SecurityUtils.getLoginUser()), socialCreditCode);
return AjaxResult.success("工商信息更新成功");
}
@Operation(summary = "启动实体工商信息历史补全")
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:backfill')")
@PostMapping("/enterprise-profile/backfill")
public AjaxResult startEnterpriseProfileBackfill() {
String taskId = enterpriseEntitySyncService.startBackfill(CallerContext.from(SecurityUtils.getLoginUser()));
return AjaxResult.success("工商信息补全任务已提交", taskId);
}
@Operation(summary = "查询实体工商信息历史补全状态")
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:backfill')")
@GetMapping("/enterprise-profile/backfill/{taskId}")
public AjaxResult getEnterpriseProfileBackfillStatus(@PathVariable String taskId) {
return AjaxResult.success(enterpriseEntitySyncService.getBackfillStatus(taskId));
}
@Operation(summary = "新增实体库信息")
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:add')")
@Log(title = "实体库管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody CcdiEnterpriseBaseInfoAddDTO addDTO) {
return toAjax(enterpriseBaseInfoService.insertEnterpriseBaseInfo(addDTO));
return toAjax(enterpriseBaseInfoService.insertEnterpriseBaseInfo(
CallerContext.from(SecurityUtils.getLoginUser()), addDTO));
}
@Operation(summary = "修改实体库信息")
@@ -86,7 +116,8 @@ public class CcdiEnterpriseBaseInfoController extends BaseController {
@Log(title = "实体库管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody CcdiEnterpriseBaseInfoEditDTO editDTO) {
return toAjax(enterpriseBaseInfoService.updateEnterpriseBaseInfo(editDTO));
return toAjax(enterpriseBaseInfoService.updateEnterpriseBaseInfo(
CallerContext.from(SecurityUtils.getLoginUser()), editDTO));
}
@Operation(summary = "删除实体库信息")
@@ -113,7 +144,8 @@ public class CcdiEnterpriseBaseInfoController extends BaseController {
return error("至少需要一条数据");
}
String taskId = enterpriseBaseInfoService.importEnterpriseBaseInfo(list);
String taskId = enterpriseBaseInfoService.importEnterpriseBaseInfo(
CallerContext.from(SecurityUtils.getLoginUser()), list);
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");

View File

@@ -5,6 +5,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -48,6 +49,30 @@ public class CcdiEnterpriseBaseInfo implements Serializable {
/** 法定代表人 */
private String legalRepresentative;
/** 注册资本 */
private BigDecimal registeredCapital;
/** 注册资本单位 */
private String registeredCapitalUnit;
/** 注册日期 */
private Date registerDate;
/** 区域编码 */
private String regionCode;
/** 区域名称 */
private String regionName;
/** 从业人数 */
private Integer employeeCount;
/** 工商缓存ID */
private Long cacheInfoId;
/** 工商同步时间 */
private Date enterpriseSyncTime;
/** 法定代表人证件类型 */
private String legalCertType;

View File

@@ -0,0 +1,21 @@
package com.ruoyi.info.collection.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/** 工商信息原始响应缓存。 */
@Data
@TableName("ccdi_enterpriseinfo_query_cache")
public class CcdiEnterpriseInfoQueryCache {
@TableId(type = IdType.AUTO)
private Long infoId;
private String queryParam;
private String queryResult;
private String queryType;
private Date createdDate;
private Date validDate;
}

View File

@@ -0,0 +1,31 @@
package com.ruoyi.info.collection.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/** 实体库企业完整股东。 */
@Data
@TableName("ccdi_enterprise_shareholder")
public class CcdiEnterpriseShareholder {
@TableId(type = IdType.AUTO)
private Long shareholderId;
private String socialCreditCode;
private Integer shareholderSeq;
private String shareholderName;
private String shareholderType;
private String shareholderCreditCode;
private BigDecimal stockPercent;
private BigDecimal subscribedCapital;
private String capitalUnit;
private Long cacheInfoId;
private Date syncTime;
private String createBy;
private Date createTime;
private String updateBy;
private Date updateTime;
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.info.collection.domain.model;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/** 统一工商信息结构。 */
@Data
public class EnterpriseProfile {
private String creditCode;
private String enterpriseName;
private BigDecimal registeredCapital;
private String registeredCapitalUnit;
private LocalDate registerDate;
private LocalDate establishDate;
private String industryCode;
private String industryName;
private String organizationTypeCode;
private String organizationTypeName;
private String regionCode;
private String regionName;
private String registerAddress;
private Integer employeeCount;
private String legalRepresentative;
private List<EnterpriseShareholderProfile> shareholders = new ArrayList<>();
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.info.collection.domain.model;
import java.util.Date;
/** 统一工商查询结果。 */
public record EnterpriseProfileQueryResult(
EnterpriseProfile profile,
Long cacheInfoId,
Date validDate,
boolean fromCache
) {
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.info.collection.domain.model;
import lombok.Data;
import java.math.BigDecimal;
/** 工商股东结构。 */
@Data
public class EnterpriseShareholderProfile {
private Integer sequence;
private String shareholderName;
private BigDecimal stockPercent;
private BigDecimal subscribedCapital;
private String capitalUnit;
}

View File

@@ -5,7 +5,10 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 实体库管理 VO
@@ -47,6 +50,39 @@ public class CcdiEnterpriseBaseInfoVO implements Serializable {
@Schema(description = "法定代表人")
private String legalRepresentative;
@Schema(description = "注册资本")
private BigDecimal registeredCapital;
@Schema(description = "注册资本单位")
private String registeredCapitalUnit;
@Schema(description = "注册日期")
private Date registerDate;
@Schema(description = "区域编码")
private String regionCode;
@Schema(description = "区域名称")
private String regionName;
@Schema(description = "从业人数")
private Integer employeeCount;
@Schema(description = "工商缓存ID")
private Long cacheInfoId;
@Schema(description = "工商同步时间")
private Date enterpriseSyncTime;
@Schema(description = "缓存有效期")
private Date enterpriseCacheValidDate;
@Schema(description = "是否允许重新查询")
private Boolean canRefreshEnterpriseProfile;
@Schema(description = "完整股东列表")
private List<EnterpriseShareholderVO> shareholders = new ArrayList<>();
@Schema(description = "法定代表人证件类型")
private String legalCertType;

View File

@@ -0,0 +1,16 @@
package com.ruoyi.info.collection.domain.vo;
import lombok.Data;
/** 工商信息历史补全任务状态。 */
@Data
public class EnterpriseProfileSyncTaskVO {
private String taskId;
private String status;
private Integer totalCount;
private Integer completedCount;
private Integer successCount;
private Integer skippedCount;
private Integer failureCount;
private String message;
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/** 企业股东明细。 */
@Data
@Schema(description = "企业股东明细")
public class EnterpriseShareholderVO {
private Integer shareholderSeq;
private String shareholderName;
private BigDecimal stockPercent;
private BigDecimal subscribedCapital;
private String capitalUnit;
}

View File

@@ -52,4 +52,8 @@ public interface CcdiEnterpriseBaseInfoMapper extends BaseMapper<CcdiEnterpriseB
* @return 更新行数
*/
int updateBatch(List<CcdiEnterpriseBaseInfo> list);
int updateEnterpriseProfile(CcdiEnterpriseBaseInfo entity);
List<CcdiEnterpriseBaseInfo> selectEnterpriseProfileCandidates();
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
/** 工商信息缓存 Mapper。 */
public interface CcdiEnterpriseInfoQueryCacheMapper extends BaseMapper<CcdiEnterpriseInfoQueryCache> {
CcdiEnterpriseInfoQueryCache selectValidCache(@Param("queryParam") String queryParam,
@Param("queryType") String queryType,
@Param("now") Date now);
int upsertCache(CcdiEnterpriseInfoQueryCache cache);
CcdiEnterpriseInfoQueryCache selectCache(@Param("queryParam") String queryParam,
@Param("queryType") String queryType);
}

View File

@@ -0,0 +1,14 @@
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.info.collection.domain.CcdiEnterpriseShareholder;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/** 实体工商股东 Mapper。 */
public interface CcdiEnterpriseShareholderMapper extends BaseMapper<CcdiEnterpriseShareholder> {
List<CcdiEnterpriseShareholder> selectByCreditCode(@Param("creditCode") String creditCode);
int deleteByCreditCode(@Param("creditCode") String creditCode);
int insertBatch(@Param("list") List<CcdiEnterpriseShareholder> list);
}

View File

@@ -4,6 +4,7 @@ import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
import com.ruoyi.info.collection.domain.vo.EnterpriseBaseInfoImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.lsfx.domain.CallerContext;
import java.util.List;
import java.util.Set;
@@ -16,7 +17,7 @@ import java.util.Set;
*/
public interface ICcdiEnterpriseBaseInfoImportService {
void importEnterpriseBaseInfoAsync(List<CcdiEnterpriseBaseInfoExcel> excelList, String taskId, String userName);
void importEnterpriseBaseInfoAsync(List<CcdiEnterpriseBaseInfoExcel> excelList, String taskId, CallerContext caller);
ImportStatusVO getImportStatus(String taskId);

View File

@@ -6,6 +6,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
import com.ruoyi.info.collection.domain.vo.CcdiEnterpriseBaseInfoVO;
import com.ruoyi.lsfx.domain.CallerContext;
import java.util.List;
@@ -22,13 +23,13 @@ public interface ICcdiEnterpriseBaseInfoService {
CcdiEnterpriseBaseInfoVO selectEnterpriseBaseInfoById(String socialCreditCode);
int insertEnterpriseBaseInfo(CcdiEnterpriseBaseInfoAddDTO addDTO);
int insertEnterpriseBaseInfo(CallerContext caller, CcdiEnterpriseBaseInfoAddDTO addDTO);
int updateEnterpriseBaseInfo(CcdiEnterpriseBaseInfoEditDTO editDTO);
int updateEnterpriseBaseInfo(CallerContext caller, CcdiEnterpriseBaseInfoEditDTO editDTO);
int deleteEnterpriseBaseInfoByIds(String[] socialCreditCodes);
List<CcdiEnterpriseBaseInfoExcel> selectEnterpriseBaseInfoListForExport(CcdiEnterpriseBaseInfoQueryDTO queryDTO);
String importEnterpriseBaseInfo(List<CcdiEnterpriseBaseInfoExcel> excelList);
String importEnterpriseBaseInfo(CallerContext caller, List<CcdiEnterpriseBaseInfoExcel> excelList);
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.vo.EnterpriseProfileSyncTaskVO;
import com.ruoyi.lsfx.domain.CallerContext;
import java.util.List;
/** 实体库工商信息同步服务。 */
public interface IEnterpriseEntitySyncService {
void submitSync(CallerContext caller, String socialCreditCode);
void submitSyncBatch(CallerContext caller, List<String> socialCreditCodes);
void refresh(CallerContext caller, String socialCreditCode);
String startBackfill(CallerContext caller);
EnterpriseProfileSyncTaskVO getBackfillStatus(String taskId);
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
import com.ruoyi.info.collection.domain.model.EnterpriseProfileQueryResult;
import com.ruoyi.lsfx.domain.CallerContext;
/** 统一工商信息查询服务。 */
public interface IEnterpriseProfileQueryService {
EnterpriseProfileQueryResult query(CallerContext caller, String enterpriseName, boolean forceRefresh);
CcdiEnterpriseInfoQueryCache findCache(String enterpriseName);
}

View File

@@ -13,6 +13,8 @@ import com.ruoyi.info.collection.enums.EnterpriseRiskLevel;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoImportService;
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
import com.ruoyi.lsfx.domain.CallerContext;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
@@ -47,9 +49,12 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
@Override
@Async
public void importEnterpriseBaseInfoAsync(List<CcdiEnterpriseBaseInfoExcel> excelList, String taskId, String userName) {
public void importEnterpriseBaseInfoAsync(List<CcdiEnterpriseBaseInfoExcel> excelList, String taskId, CallerContext caller) {
List<CcdiEnterpriseBaseInfo> successRecords = new ArrayList<>();
List<EnterpriseBaseInfoImportFailureVO> failures = new ArrayList<>();
Set<String> existingCreditCodes = getExistingCreditCodes(excelList);
@@ -57,7 +62,7 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
for (CcdiEnterpriseBaseInfoExcel excel : excelList) {
try {
CcdiEnterpriseBaseInfo entity = validateAndBuildEntity(excel, existingCreditCodes, processedCreditCodes, userName);
CcdiEnterpriseBaseInfo entity = validateAndBuildEntity(excel, existingCreditCodes, processedCreditCodes, caller.username());
successRecords.add(entity);
processedCreditCodes.add(entity.getSocialCreditCode());
} catch (Exception e) {
@@ -70,6 +75,9 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
if (!successRecords.isEmpty()) {
saveBatch(successRecords, 500);
enterpriseEntitySyncService.submitSyncBatch(caller, successRecords.stream()
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
.toList());
}
if (!failures.isEmpty()) {

View File

@@ -2,10 +2,10 @@ package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiCustEnterpriseRelation;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoAddDTO;
@@ -13,20 +13,27 @@ import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
import com.ruoyi.info.collection.domain.vo.CcdiEnterpriseBaseInfoVO;
import com.ruoyi.info.collection.domain.vo.EnterpriseShareholderVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.enums.EnterpriseRiskLevel;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseShareholderMapper;
import com.ruoyi.info.collection.mapper.CcdiCustEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoImportService;
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoService;
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
import com.ruoyi.info.collection.service.IEnterpriseProfileQueryService;
import com.ruoyi.lsfx.domain.CallerContext;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.HashMap;
import java.util.List;
@@ -62,6 +69,15 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private CcdiEnterpriseShareholderMapper enterpriseShareholderMapper;
@Resource
private IEnterpriseProfileQueryService enterpriseProfileQueryService;
@Resource
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
@Override
public Page<CcdiEnterpriseBaseInfoVO> selectEnterpriseBaseInfoPage(Page<CcdiEnterpriseBaseInfoVO> page,
CcdiEnterpriseBaseInfoQueryDTO queryDTO) {
@@ -76,12 +92,21 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
}
CcdiEnterpriseBaseInfoVO vo = new CcdiEnterpriseBaseInfoVO();
BeanUtils.copyProperties(entity, vo);
vo.setShareholders(enterpriseShareholderMapper.selectByCreditCode(socialCreditCode).stream().map(item -> {
EnterpriseShareholderVO shareholder = new EnterpriseShareholderVO();
BeanUtils.copyProperties(item, shareholder);
return shareholder;
}).toList());
CcdiEnterpriseInfoQueryCache cache = enterpriseProfileQueryService.findCache(entity.getEnterpriseName());
vo.setEnterpriseCacheValidDate(cache == null ? null : cache.getValidDate());
vo.setCanRefreshEnterpriseProfile(entity.getEnterpriseSyncTime() == null || cache == null
|| cache.getValidDate() == null || !cache.getValidDate().after(new java.util.Date()));
return vo;
}
@Override
@Transactional
public int insertEnterpriseBaseInfo(CcdiEnterpriseBaseInfoAddDTO addDTO) {
public int insertEnterpriseBaseInfo(CallerContext caller, CcdiEnterpriseBaseInfoAddDTO addDTO) {
if (enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode()) != null) {
throw new RuntimeException("该统一社会信用代码已存在");
}
@@ -91,12 +116,16 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
BeanUtils.copyProperties(addDTO, entity);
entity.setStatus(trimToNull(addDTO.getStatus()));
entity.setDataSource(DataSource.MANUAL.getCode());
return enterpriseBaseInfoMapper.insert(entity);
int rows = enterpriseBaseInfoMapper.insert(entity);
if (rows > 0) {
submitSyncAfterCommit(caller, entity.getSocialCreditCode());
}
return rows;
}
@Override
@Transactional
public int updateEnterpriseBaseInfo(CcdiEnterpriseBaseInfoEditDTO editDTO) {
public int updateEnterpriseBaseInfo(CallerContext caller, CcdiEnterpriseBaseInfoEditDTO editDTO) {
CcdiEnterpriseBaseInfo existing = enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode());
if (existing == null) {
throw new RuntimeException("实体库记录不存在");
@@ -107,7 +136,11 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
BeanUtils.copyProperties(editDTO, entity);
entity.setStatus(trimToNull(editDTO.getStatus()));
entity.setDataSource(existing.getDataSource());
return enterpriseBaseInfoMapper.updateById(entity);
int rows = enterpriseBaseInfoMapper.updateById(entity);
if (rows > 0 && !java.util.Objects.equals(trimToNull(existing.getEnterpriseName()), trimToNull(entity.getEnterpriseName()))) {
submitSyncAfterCommit(caller, entity.getSocialCreditCode());
}
return rows;
}
@Override
@@ -118,6 +151,7 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
}
for (String socialCreditCode : socialCreditCodes) {
validateDeleteRelations(socialCreditCode);
enterpriseShareholderMapper.deleteByCreditCode(socialCreditCode);
}
return enterpriseBaseInfoMapper.deleteBatchIds(List.of(socialCreditCodes));
}
@@ -134,7 +168,7 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
@Override
@Transactional
public String importEnterpriseBaseInfo(List<CcdiEnterpriseBaseInfoExcel> excelList) {
public String importEnterpriseBaseInfo(CallerContext caller, List<CcdiEnterpriseBaseInfoExcel> excelList) {
String taskId = UUID.randomUUID().toString();
String statusKey = "import:enterpriseBaseInfo:" + taskId;
@@ -151,10 +185,24 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
enterpriseBaseInfoImportService.importEnterpriseBaseInfoAsync(excelList, taskId, SecurityUtils.getUsername());
enterpriseBaseInfoImportService.importEnterpriseBaseInfoAsync(
excelList, taskId, caller);
return taskId;
}
private void submitSyncAfterCommit(CallerContext caller, String socialCreditCode) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
enterpriseEntitySyncService.submitSync(caller, socialCreditCode);
}
});
} else {
enterpriseEntitySyncService.submitSync(caller, socialCreditCode);
}
}
private LambdaQueryWrapper<CcdiEnterpriseBaseInfo> buildQueryWrapper(CcdiEnterpriseBaseInfoQueryDTO queryDTO) {
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
if (queryDTO == null) {

View File

@@ -0,0 +1,304 @@
package com.ruoyi.info.collection.service.impl;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
import com.ruoyi.info.collection.domain.CcdiEnterpriseShareholder;
import com.ruoyi.info.collection.domain.model.EnterpriseProfile;
import com.ruoyi.info.collection.domain.model.EnterpriseProfileQueryResult;
import com.ruoyi.info.collection.domain.model.EnterpriseShareholderProfile;
import com.ruoyi.info.collection.domain.vo.EnterpriseProfileSyncTaskVO;
import com.ruoyi.info.collection.enums.DataSource;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseShareholderMapper;
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
import com.ruoyi.info.collection.service.IEnterpriseProfileQueryService;
import com.ruoyi.lsfx.domain.CallerContext;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.StringUtils;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/** 实体库工商信息同步服务实现。 */
@Service
public class EnterpriseEntitySyncServiceImpl implements IEnterpriseEntitySyncService {
private static final String TASK_PREFIX = "enterprise:profile:backfill:";
@Resource
private CcdiEnterpriseBaseInfoMapper enterpriseMapper;
@Resource
private CcdiEnterpriseShareholderMapper shareholderMapper;
@Resource
private IEnterpriseProfileQueryService queryService;
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
@Qualifier("enterpriseProfileExecutor")
private Executor enterpriseProfileExecutor;
@Override
public void submitSync(CallerContext caller, String socialCreditCode) {
submitSyncBatch(caller, List.of(socialCreditCode));
}
@Override
public void submitSyncBatch(CallerContext caller, List<String> socialCreditCodes) {
if (socialCreditCodes == null || socialCreditCodes.isEmpty()) {
return;
}
List<String> normalizedCodes = socialCreditCodes.stream().filter(StringUtils::hasText).distinct().toList();
if (normalizedCodes.isEmpty()) {
return;
}
List<CcdiEnterpriseBaseInfo> entities = enterpriseMapper.selectBatchIds(normalizedCodes);
Map<String, List<CcdiEnterpriseBaseInfo>> groups = entities.stream()
.filter(item -> StringUtils.hasText(item.getEnterpriseName()))
.collect(Collectors.groupingBy(item -> item.getEnterpriseName().trim(), LinkedHashMap::new, Collectors.toList()));
groups.forEach((name, sameNameEntities) -> CompletableFuture.runAsync(() -> {
EnterpriseProfileQueryResult result = queryService.query(caller, name, false);
sameNameEntities.stream()
.filter(item -> result.profile().getCreditCode().equals(item.getSocialCreditCode()))
.findFirst()
.ifPresent(item -> applyProfile(item, result, caller.username()));
}, enterpriseProfileExecutor));
}
@Override
public void refresh(CallerContext caller, String socialCreditCode) {
CcdiEnterpriseBaseInfo entity = requireEntity(socialCreditCode);
CcdiEnterpriseInfoQueryCache cache = queryService.findCache(entity.getEnterpriseName());
boolean cacheValid = cache != null && cache.getValidDate() != null && cache.getValidDate().after(new Date());
if (entity.getEnterpriseSyncTime() != null && cacheValid) {
throw new IllegalStateException("工商缓存仍在有效期内,无需重新查询");
}
SyncOutcome outcome = syncByCreditCode(caller, socialCreditCode, !cacheValid);
if (outcome != SyncOutcome.SUCCESS) {
throw new IllegalStateException("工商信用代码与实体主键不一致");
}
}
@Override
public String startBackfill(CallerContext caller) {
String taskId = UUID.randomUUID().toString().replace("-", "");
List<CcdiEnterpriseBaseInfo> candidates = enterpriseMapper.selectEnterpriseProfileCandidates();
Map<String, List<CcdiEnterpriseBaseInfo>> groups = candidates.stream()
.filter(item -> StringUtils.hasText(item.getEnterpriseName()))
.collect(Collectors.groupingBy(item -> item.getEnterpriseName().trim(), LinkedHashMap::new, Collectors.toList()));
initializeTask(taskId, groups.size());
CompletableFuture.runAsync(() -> runBackfill(taskId, caller, groups));
return taskId;
}
@Override
public EnterpriseProfileSyncTaskVO getBackfillStatus(String taskId) {
Map<Object, Object> status = redisTemplate.opsForHash().entries(taskKey(taskId));
if (status.isEmpty()) {
throw new IllegalArgumentException("任务不存在或已过期");
}
EnterpriseProfileSyncTaskVO vo = new EnterpriseProfileSyncTaskVO();
vo.setTaskId(string(status.get("taskId")));
vo.setStatus(string(status.get("status")));
vo.setTotalCount(number(status.get("totalCount")));
vo.setCompletedCount(number(status.get("completedCount")));
vo.setSuccessCount(number(status.get("successCount")));
vo.setSkippedCount(number(status.get("skippedCount")));
vo.setFailureCount(number(status.get("failureCount")));
vo.setMessage(string(status.get("message")));
return vo;
}
private void runBackfill(String taskId, CallerContext caller,
Map<String, List<CcdiEnterpriseBaseInfo>> groups) {
AtomicInteger completed = new AtomicInteger();
AtomicInteger success = new AtomicInteger();
AtomicInteger skipped = new AtomicInteger();
AtomicInteger failure = new AtomicInteger();
List<Map.Entry<String, List<CcdiEnterpriseBaseInfo>>> entries = new ArrayList<>(groups.entrySet());
for (int start = 0; start < entries.size(); start += 100) {
int end = Math.min(start + 100, entries.size());
List<CompletableFuture<Void>> futures = entries.subList(start, end).stream()
.map(entry -> CompletableFuture.runAsync(() -> {
try {
EnterpriseProfileQueryResult result = queryService.query(caller, entry.getKey(), false);
CcdiEnterpriseBaseInfo match = entry.getValue().stream()
.filter(item -> result.profile().getCreditCode().equals(item.getSocialCreditCode()))
.findFirst().orElse(null);
if (match == null) {
skipped.incrementAndGet();
} else {
applyProfile(match, result, caller.username());
success.incrementAndGet();
}
} catch (Exception e) {
failure.incrementAndGet();
} finally {
completed.incrementAndGet();
updateTask(taskId, "PROCESSING", completed.get(), success.get(), skipped.get(), failure.get());
}
}, enterpriseProfileExecutor)).toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
updateTask(taskId, "COMPLETED", completed.get(), success.get(), skipped.get(), failure.get());
}
private SyncOutcome syncByCreditCode(CallerContext caller, String socialCreditCode,
boolean forceRefresh) {
CcdiEnterpriseBaseInfo entity = requireEntity(socialCreditCode);
EnterpriseProfileQueryResult result = queryService.query(caller, entity.getEnterpriseName(), forceRefresh);
if (!result.profile().getCreditCode().equals(entity.getSocialCreditCode())) {
return SyncOutcome.SKIPPED;
}
applyProfile(entity, result, caller.username());
return SyncOutcome.SUCCESS;
}
private void applyProfile(CcdiEnterpriseBaseInfo existing, EnterpriseProfileQueryResult result, String username) {
transactionTemplate.executeWithoutResult(status -> {
EnterpriseProfile profile = result.profile();
Date now = new Date();
CcdiEnterpriseBaseInfo update = new CcdiEnterpriseBaseInfo();
update.setSocialCreditCode(existing.getSocialCreditCode());
update.setEnterpriseName(existing.getEnterpriseName().trim());
update.setEnterpriseType(profile.getOrganizationTypeName());
update.setIndustryClass(profile.getIndustryCode());
update.setIndustryName(profile.getIndustryName());
update.setEstablishDate(toDate(profile.getEstablishDate()));
update.setRegisterAddress(profile.getRegisterAddress());
update.setLegalRepresentative(profile.getLegalRepresentative());
update.setRegisteredCapital(profile.getRegisteredCapital());
update.setRegisteredCapitalUnit(profile.getRegisteredCapitalUnit());
update.setRegisterDate(toDate(profile.getRegisterDate()));
update.setRegionCode(profile.getRegionCode());
update.setRegionName(profile.getRegionName());
update.setEmployeeCount(profile.getEmployeeCount());
update.setCacheInfoId(result.cacheInfoId());
update.setEnterpriseSyncTime(now);
update.setDataSource(DataSource.API.getCode());
update.setUpdatedBy(username);
setTopFive(update, profile.getShareholders());
if (enterpriseMapper.updateEnterpriseProfile(update) != 1) {
throw new IllegalStateException("实体工商信息更新失败");
}
shareholderMapper.deleteByCreditCode(existing.getSocialCreditCode());
List<CcdiEnterpriseShareholder> shareholders = profile.getShareholders().stream()
.map(item -> toEntityShareholder(existing.getSocialCreditCode(), item, result.cacheInfoId(), now, username))
.toList();
if (!shareholders.isEmpty()) {
shareholderMapper.insertBatch(shareholders);
}
});
}
private CcdiEnterpriseShareholder toEntityShareholder(String creditCode,
EnterpriseShareholderProfile item,
Long cacheInfoId,
Date now,
String username) {
CcdiEnterpriseShareholder shareholder = new CcdiEnterpriseShareholder();
shareholder.setSocialCreditCode(creditCode);
shareholder.setShareholderSeq(item.getSequence());
shareholder.setShareholderName(item.getShareholderName());
shareholder.setStockPercent(item.getStockPercent());
shareholder.setSubscribedCapital(item.getSubscribedCapital());
shareholder.setCapitalUnit(item.getCapitalUnit());
shareholder.setCacheInfoId(cacheInfoId);
shareholder.setSyncTime(now);
shareholder.setCreateBy(username);
shareholder.setCreateTime(now);
shareholder.setUpdateBy(username);
shareholder.setUpdateTime(now);
return shareholder;
}
private void setTopFive(CcdiEnterpriseBaseInfo entity, List<EnterpriseShareholderProfile> shareholders) {
String[] names = new String[5];
for (int i = 0; i < Math.min(names.length, shareholders.size()); i++) {
names[i] = shareholders.get(i).getShareholderName();
}
entity.setShareholder1(names[0]);
entity.setShareholder2(names[1]);
entity.setShareholder3(names[2]);
entity.setShareholder4(names[3]);
entity.setShareholder5(names[4]);
}
private CcdiEnterpriseBaseInfo requireEntity(String socialCreditCode) {
CcdiEnterpriseBaseInfo entity = enterpriseMapper.selectById(socialCreditCode);
if (entity == null) {
throw new IllegalArgumentException("实体库记录不存在");
}
if (!StringUtils.hasText(entity.getEnterpriseName())) {
throw new IllegalArgumentException("企业名称不能为空");
}
return entity;
}
private Date toDate(java.time.LocalDate value) {
return value == null ? null : Date.from(value.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
private void initializeTask(String taskId, int totalCount) {
Map<String, Object> values = new LinkedHashMap<>();
values.put("taskId", taskId);
values.put("status", "PROCESSING");
values.put("totalCount", totalCount);
values.put("completedCount", 0);
values.put("successCount", 0);
values.put("skippedCount", 0);
values.put("failureCount", 0);
values.put("message", "正在补全工商信息");
redisTemplate.opsForHash().putAll(taskKey(taskId), values);
redisTemplate.expire(taskKey(taskId), 7, TimeUnit.DAYS);
}
private void updateTask(String taskId, String status, int completed, int success, int skipped, int failure) {
Map<String, Object> values = new LinkedHashMap<>();
values.put("status", status);
values.put("completedCount", completed);
values.put("successCount", success);
values.put("skippedCount", skipped);
values.put("failureCount", failure);
values.put("message", "COMPLETED".equals(status) ? "工商信息补全完成" : "正在补全工商信息");
redisTemplate.opsForHash().putAll(taskKey(taskId), values);
}
private String taskKey(String taskId) {
return TASK_PREFIX + taskId;
}
private String string(Object value) {
return value == null ? null : value.toString();
}
private Integer number(Object value) {
return value == null ? 0 : Integer.valueOf(value.toString());
}
private enum SyncOutcome {
SUCCESS, SKIPPED
}
}

View File

@@ -0,0 +1,92 @@
package com.ruoyi.info.collection.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
import com.ruoyi.info.collection.domain.model.EnterpriseProfile;
import com.ruoyi.info.collection.domain.model.EnterpriseProfileQueryResult;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseInfoQueryCacheMapper;
import com.ruoyi.info.collection.service.IEnterpriseProfileQueryService;
import com.ruoyi.info.collection.service.support.XinhuaEnterpriseProfileParser;
import com.ruoyi.lsfx.client.XinhuaEnterpriseClient;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.domain.response.XinhuaEnterpriseQueryResult;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
/** 统一工商信息查询服务实现。 */
@Service
public class EnterpriseProfileQueryServiceImpl implements IEnterpriseProfileQueryService {
public static final String QUERY_TYPE = "EnterpriseProfile";
private static final int CACHE_VALID_DAYS = 180;
@Resource
private CcdiEnterpriseInfoQueryCacheMapper cacheMapper;
@Resource
private XinhuaEnterpriseClient xinhuaEnterpriseClient;
@Resource
private XinhuaEnterpriseProfileParser profileParser;
@Resource
private ObjectMapper objectMapper;
@Override
public EnterpriseProfileQueryResult query(CallerContext caller, String enterpriseName, boolean forceRefresh) {
String normalizedName = normalize(enterpriseName);
if (!StringUtils.hasText(normalizedName)) {
throw new IllegalArgumentException("企业名称不能为空");
}
Date now = new Date();
if (!forceRefresh) {
CcdiEnterpriseInfoQueryCache cache = cacheMapper.selectValidCache(normalizedName, QUERY_TYPE, now);
if (cache != null) {
return fromCache(cache);
}
}
XinhuaEnterpriseQueryResult external = xinhuaEnterpriseClient.query(caller, normalizedName);
EnterpriseProfile profile = profileParser.parse(external.profile());
Date successTime = new Date();
Date validDate = Date.from(LocalDateTime.ofInstant(successTime.toInstant(), ZoneId.systemDefault())
.plusDays(CACHE_VALID_DAYS)
.atZone(ZoneId.systemDefault()).toInstant());
CcdiEnterpriseInfoQueryCache cache = new CcdiEnterpriseInfoQueryCache();
cache.setQueryParam(normalizedName);
cache.setQueryType(QUERY_TYPE);
cache.setQueryResult(external.rawJson());
cache.setCreatedDate(successTime);
cache.setValidDate(validDate);
cacheMapper.upsertCache(cache);
CcdiEnterpriseInfoQueryCache saved = cacheMapper.selectCache(normalizedName, QUERY_TYPE);
return new EnterpriseProfileQueryResult(profile, saved.getInfoId(), saved.getValidDate(), false);
}
@Override
public CcdiEnterpriseInfoQueryCache findCache(String enterpriseName) {
String normalizedName = normalize(enterpriseName);
return StringUtils.hasText(normalizedName) ? cacheMapper.selectCache(normalizedName, QUERY_TYPE) : null;
}
private EnterpriseProfileQueryResult fromCache(CcdiEnterpriseInfoQueryCache cache) {
try {
JsonNode root = objectMapper.readTree(cache.getQueryResult());
EnterpriseProfile profile = profileParser.parse(root.path("data").path("mappingOutputFields"));
return new EnterpriseProfileQueryResult(profile, cache.getInfoId(), cache.getValidDate(), true);
} catch (Exception e) {
throw new IllegalStateException("工商缓存内容无法解析: infoId=" + cache.getInfoId(), e);
}
}
private String normalize(String value) {
return value == null ? null : value.trim();
}
}

View File

@@ -4,10 +4,14 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.info.collection.enums.EnterpriseSource;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
import com.ruoyi.lsfx.domain.CallerContext;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -27,6 +31,9 @@ public class EnterpriseAutoFillService {
@Resource
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
@Resource
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
public record EnterpriseFillItem(
String socialCreditCode,
String enterpriseName,
@@ -63,6 +70,7 @@ public class EnterpriseAutoFillService {
return;
}
insertBatchIgnoreDuplicate(missingEntities);
submitSyncAfterCommit(missingEntities);
}
private Map<String, EnterpriseFillItem> normalizeItems(List<EnterpriseFillItem> items) {
@@ -121,6 +129,23 @@ public class EnterpriseAutoFillService {
}
}
private void submitSyncAfterCommit(List<CcdiEnterpriseBaseInfo> entities) {
String username = entities.stream().map(CcdiEnterpriseBaseInfo::getCreatedBy)
.filter(StringUtils::isNotEmpty).findFirst().orElse("system");
CallerContext caller = CallerContext.of(null, username);
List<String> creditCodes = entities.stream().map(CcdiEnterpriseBaseInfo::getSocialCreditCode).toList();
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
enterpriseEntitySyncService.submitSyncBatch(caller, creditCodes);
return;
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
enterpriseEntitySyncService.submitSyncBatch(caller, creditCodes);
}
});
}
private String trimToNull(String value) {
if (StringUtils.isEmpty(value)) {
return null;

View File

@@ -0,0 +1,141 @@
package com.ruoyi.info.collection.service.support;
import com.fasterxml.jackson.databind.JsonNode;
import com.ruoyi.info.collection.domain.model.EnterpriseProfile;
import com.ruoyi.info.collection.domain.model.EnterpriseShareholderProfile;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/** 将新华社 mappingOutputFields 解析为统一工商结构。 */
@Component
public class XinhuaEnterpriseProfileParser {
public EnterpriseProfile parse(JsonNode node) {
if (node == null || !node.isObject() || node.isEmpty()) {
throw new IllegalArgumentException("工商信息对象不能为空");
}
EnterpriseProfile profile = new EnterpriseProfile();
profile.setCreditCode(text(node, "creditCode"));
profile.setEnterpriseName(text(node, "enterpriseName"));
JsonNode capital = node.path("regCapitalAmount");
profile.setRegisteredCapital(decimal(text(capital, "regist_capi_value"), false));
profile.setRegisteredCapitalUnit(text(capital, "regist_capi_unit"));
profile.setRegisterDate(date(text(node, "estiblishTime")));
profile.setEstablishDate(date(text(node, "fromTime")));
JsonNode industry = node.path("industry");
profile.setIndustryCode(text(industry, "industry_code"));
profile.setIndustryName(text(industry, "industry"));
JsonNode orgType = node.path("companyOrgType");
if (orgType.isObject()) {
profile.setOrganizationTypeCode(text(orgType, "econ_kind_code"));
profile.setOrganizationTypeName(text(orgType, "econ_kind"));
} else if (orgType.isTextual()) {
profile.setOrganizationTypeName(normalize(orgType.asText()));
}
JsonNode city = node.path("baseCity");
profile.setRegionCode(text(city, "province_code"));
profile.setRegionName(regionName(city));
profile.setRegisterAddress(text(node, "regLocation"));
profile.setEmployeeCount(integer(node.get("employeeCount")));
profile.setLegalRepresentative(text(node, "legalPersonName"));
profile.setShareholders(shareholders(node.path("holders")));
return profile;
}
private List<EnterpriseShareholderProfile> shareholders(JsonNode holders) {
List<EnterpriseShareholderProfile> result = new ArrayList<>();
if (!holders.isArray()) {
return result;
}
int sequence = 1;
for (JsonNode holder : holders) {
String name = text(holder, "stock_name");
if (!StringUtils.hasText(name)) {
continue;
}
EnterpriseShareholderProfile item = new EnterpriseShareholderProfile();
item.setSequence(sequence++);
item.setShareholderName(name);
item.setStockPercent(decimal(text(holder, "stock_percent"), true));
item.setSubscribedCapital(decimal(text(holder, "should_capi"), false));
item.setCapitalUnit("万元");
result.add(item);
}
return result;
}
private String regionName(JsonNode city) {
Set<String> values = new LinkedHashSet<>();
add(values, text(city, "province"));
add(values, text(city, "city"));
add(values, text(city, "county"));
return values.isEmpty() ? null : String.join("", values);
}
private void add(Set<String> values, String value) {
if (StringUtils.hasText(value)) {
values.add(value);
}
}
private String text(JsonNode node, String field) {
if (node == null || !node.isObject()) {
return null;
}
JsonNode value = node.get(field);
return value == null || value.isNull() ? null : normalize(value.asText());
}
private String normalize(String value) {
return value == null ? null : value.trim();
}
private BigDecimal decimal(String value, boolean percentage) {
if (!StringUtils.hasText(value)) {
return null;
}
String normalized = value.replace(",", "").trim();
if (percentage && normalized.endsWith("%")) {
normalized = normalized.substring(0, normalized.length() - 1).trim();
}
try {
return new BigDecimal(normalized);
} catch (NumberFormatException e) {
return null;
}
}
private Integer integer(JsonNode value) {
if (value == null || value.isNull()) {
return null;
}
try {
return Integer.valueOf(value.asText().trim());
} catch (NumberFormatException e) {
return null;
}
}
private LocalDate date(String value) {
if (!StringUtils.hasText(value) || value.length() < 10) {
return null;
}
try {
return LocalDate.parse(value.substring(0, 10));
} catch (DateTimeParseException e) {
return null;
}
}
}

View File

@@ -14,6 +14,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="establishDate" column="establish_date"/>
<result property="registerAddress" column="register_address"/>
<result property="legalRepresentative" column="legal_representative"/>
<result property="registeredCapital" column="registered_capital"/>
<result property="registeredCapitalUnit" column="registered_capital_unit"/>
<result property="registerDate" column="register_date"/>
<result property="regionCode" column="region_code"/>
<result property="regionName" column="region_name"/>
<result property="employeeCount" column="employee_count"/>
<result property="cacheInfoId" column="cache_info_id"/>
<result property="enterpriseSyncTime" column="enterprise_sync_time"/>
<result property="legalCertType" column="legal_cert_type"/>
<result property="legalCertNo" column="legal_cert_no"/>
<result property="shareholder1" column="shareholder1"/>
@@ -39,6 +47,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
establish_date,
register_address,
legal_representative,
registered_capital,
registered_capital_unit,
register_date,
region_code,
region_name,
employee_count,
cache_info_id,
enterprise_sync_time,
legal_cert_type,
legal_cert_no,
shareholder1,
@@ -178,4 +194,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</update>
<update id="updateEnterpriseProfile" parameterType="com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo">
update ccdi_enterprise_base_info
set enterprise_type = #{enterpriseType},
industry_class = #{industryClass},
industry_name = #{industryName},
establish_date = #{establishDate},
register_address = #{registerAddress},
legal_representative = #{legalRepresentative},
registered_capital = #{registeredCapital},
registered_capital_unit = #{registeredCapitalUnit},
register_date = #{registerDate},
region_code = #{regionCode},
region_name = #{regionName},
employee_count = #{employeeCount},
cache_info_id = #{cacheInfoId},
enterprise_sync_time = #{enterpriseSyncTime},
shareholder1 = #{shareholder1},
shareholder2 = #{shareholder2},
shareholder3 = #{shareholder3},
shareholder4 = #{shareholder4},
shareholder5 = #{shareholder5},
data_source = #{dataSource},
updated_by = #{updatedBy},
update_time = now()
where social_credit_code = #{socialCreditCode}
and trim(enterprise_name) = trim(#{enterpriseName})
</update>
<select id="selectEnterpriseProfileCandidates" resultType="com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo">
select social_credit_code, enterprise_name, cache_info_id, enterprise_sync_time
from ccdi_enterprise_base_info
where enterprise_name is not null and trim(enterprise_name) != ''
order by enterprise_name, social_credit_code
</select>
</mapper>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiEnterpriseInfoQueryCacheMapper">
<select id="selectValidCache" resultType="com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache">
select info_id, query_param, query_result, query_type, created_date, valid_date
from ccdi_enterpriseinfo_query_cache
where query_param = #{queryParam}
and query_type = #{queryType}
and valid_date &gt; #{now}
limit 1
</select>
<select id="selectCache" resultType="com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache">
select info_id, query_param, query_result, query_type, created_date, valid_date
from ccdi_enterpriseinfo_query_cache
where query_param = #{queryParam}
and query_type = #{queryType}
limit 1
</select>
<insert id="upsertCache" parameterType="com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache">
insert into ccdi_enterpriseinfo_query_cache
(query_param, query_result, query_type, created_date, valid_date)
values
(#{queryParam}, #{queryResult}, #{queryType}, #{createdDate}, #{validDate})
on duplicate key update
query_result = values(query_result),
created_date = values(created_date),
valid_date = values(valid_date)
</insert>
</mapper>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiEnterpriseShareholderMapper">
<select id="selectByCreditCode" resultType="com.ruoyi.info.collection.domain.CcdiEnterpriseShareholder">
select shareholder_id, social_credit_code, shareholder_seq, shareholder_name,
shareholder_type, shareholder_credit_code, stock_percent, subscribed_capital,
capital_unit, cache_info_id, sync_time, create_by, create_time, update_by, update_time
from ccdi_enterprise_shareholder
where social_credit_code = #{creditCode}
order by shareholder_seq
</select>
<delete id="deleteByCreditCode">
delete from ccdi_enterprise_shareholder where social_credit_code = #{creditCode}
</delete>
<insert id="insertBatch">
insert into ccdi_enterprise_shareholder (
social_credit_code, shareholder_seq, shareholder_name, shareholder_type,
shareholder_credit_code, stock_percent, subscribed_capital, capital_unit,
cache_info_id, sync_time, create_by, create_time, update_by, update_time
) values
<foreach collection="list" item="item" separator=",">
(#{item.socialCreditCode}, #{item.shareholderSeq}, #{item.shareholderName}, #{item.shareholderType},
#{item.shareholderCreditCode}, #{item.stockPercent}, #{item.subscribedCapital}, #{item.capitalUnit},
#{item.cacheInfoId}, #{item.syncTime}, #{item.createBy}, #{item.createTime}, #{item.updateBy}, #{item.updateTime})
</foreach>
</insert>
</mapper>

View File

@@ -6,9 +6,11 @@ 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.CcdiEnterpriseShareholderMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
import com.ruoyi.info.collection.service.impl.CcdiEnterpriseBaseInfoServiceImpl;
import com.ruoyi.lsfx.domain.CallerContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@@ -49,13 +51,22 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private CcdiEnterpriseShareholderMapper enterpriseShareholderMapper;
@Mock
private IEnterpriseProfileQueryService enterpriseProfileQueryService;
@Mock
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
@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);
int result = service.insertEnterpriseBaseInfo(CallerContext.system(), addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
@@ -72,7 +83,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
when(enterpriseBaseInfoMapper.insert(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
int result = service.insertEnterpriseBaseInfo(addDTO);
int result = service.insertEnterpriseBaseInfo(CallerContext.system(), addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
@@ -88,7 +99,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.insertEnterpriseBaseInfo(addDTO));
() -> service.insertEnterpriseBaseInfo(CallerContext.system(), addDTO));
assertEquals("风险等级不在允许范围内", exception.getMessage());
}
@@ -99,7 +110,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
when(enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode())).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.updateEnterpriseBaseInfo(editDTO));
() -> service.updateEnterpriseBaseInfo(CallerContext.system(), editDTO));
assertEquals("实体库记录不存在", exception.getMessage());
}
@@ -114,7 +125,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
when(enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode())).thenReturn(existing);
when(enterpriseBaseInfoMapper.updateById(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
int result = service.updateEnterpriseBaseInfo(editDTO);
int result = service.updateEnterpriseBaseInfo(CallerContext.system(), editDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
@@ -130,6 +141,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
entity.setRiskLevel("1");
entity.setEntSource("GENERAL");
when(enterpriseBaseInfoMapper.selectById("91310000123456789A")).thenReturn(entity);
when(enterpriseShareholderMapper.selectByCreditCode("91310000123456789A")).thenReturn(java.util.List.of());
CcdiEnterpriseBaseInfoVO vo = service.selectEnterpriseBaseInfoById("91310000123456789A");