docs: 添加采购交易导入功能优化设计文档

设计目标:
- 采用后台异步处理+通知提示,避免弹窗阻塞用户操作
- 完全复用员工信息维护的导入逻辑
- 支持查看导入失败记录
- 实现状态持久化

主要设计内容:
- 整体架构和用户交互流程
- 前端组件结构和状态管理
- UI组件修改方案
- 核心方法实现(10个方法)
- 完整修改清单和测试要点

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wkc
2026-02-08 13:40:32 +08:00
parent 89399cab67
commit e120f836b2
9 changed files with 1526 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
package com.ruoyi.ccdi.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 实体中介导入失败记录VO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "实体中介导入失败记录")
public class IntermediaryEntityImportFailureVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "机构名称")
private String enterpriseName;
@Schema(description = "统一社会信用代码")
private String socialCreditCode;
@Schema(description = "主体类型")
private String enterpriseType;
@Schema(description = "企业性质")
private String enterpriseNature;
@Schema(description = "法定代表人")
private String legalRepresentative;
@Schema(description = "成立日期")
private Date establishDate;
@Schema(description = "错误信息")
private String errorMessage;
}

View File

@@ -0,0 +1,42 @@
package com.ruoyi.ccdi.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 个人中介导入失败记录VO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "个人中介导入失败记录")
public class IntermediaryPersonImportFailureVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "姓名")
private String name;
@Schema(description = "证件号码")
private String personId;
@Schema(description = "人员类型")
private String personType;
@Schema(description = "性别")
private String gender;
@Schema(description = "手机号码")
private String mobile;
@Schema(description = "所在公司")
private String company;
@Schema(description = "错误信息")
private String errorMessage;
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.ccdi.service;
import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryEntityExcel;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.IntermediaryEntityImportFailureVO;
import java.util.List;
/**
* 实体中介异步导入Service接口
*
* @author ruoyi
* @date 2026-02-06
*/
public interface ICcdiIntermediaryEntityImportService {
/**
* 异步导入实体中介数据
*
* @param excelList Excel数据列表
* @param isUpdateSupport 是否更新已存在的数据
* @param taskId 任务ID
* @param userName 当前用户名(用于审计字段)
*/
void importEntityAsync(List<CcdiIntermediaryEntityExcel> excelList,
Boolean isUpdateSupport,
String taskId,
String userName);
/**
* 查询导入状态
*
* @param taskId 任务ID
* @return 导入状态信息
*/
ImportStatusVO getImportStatus(String taskId);
/**
* 获取导入失败记录
*
* @param taskId 任务ID
* @return 失败记录列表
*/
List<IntermediaryEntityImportFailureVO> getImportFailures(String taskId);
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.ccdi.service;
import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryPersonExcel;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.IntermediaryPersonImportFailureVO;
import java.util.List;
/**
* 个人中介异步导入Service接口
*
* @author ruoyi
* @date 2026-02-06
*/
public interface ICcdiIntermediaryPersonImportService {
/**
* 异步导入个人中介数据
*
* @param excelList Excel数据列表
* @param isUpdateSupport 是否更新已存在的数据
* @param taskId 任务ID
* @param userName 当前用户名(用于审计字段)
*/
void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
Boolean isUpdateSupport,
String taskId,
String userName);
/**
* 查询导入状态
*
* @param taskId 任务ID
* @return 导入状态信息
*/
ImportStatusVO getImportStatus(String taskId);
/**
* 获取导入失败记录
*
* @param taskId 任务ID
* @return 失败记录列表
*/
List<IntermediaryPersonImportFailureVO> getImportFailures(String taskId);
}

View File

@@ -0,0 +1,253 @@
package com.ruoyi.ccdi.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.ccdi.domain.CcdiEnterpriseBaseInfo;
import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryEntityExcel;
import com.ruoyi.ccdi.domain.vo.ImportResult;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.IntermediaryEntityImportFailureVO;
import com.ruoyi.ccdi.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.ccdi.service.ICcdiIntermediaryEntityImportService;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 实体中介异步导入Service实现
*
* @author ruoyi
* @date 2026-02-06
*/
@Service
@EnableAsync
public class CcdiIntermediaryEntityImportServiceImpl implements ICcdiIntermediaryEntityImportService {
@Resource
private CcdiEnterpriseBaseInfoMapper entityMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
@Async
@Transactional(rollbackFor = Exception.class)
public void importEntityAsync(List<CcdiIntermediaryEntityExcel> excelList,
Boolean isUpdateSupport,
String taskId,
String userName) {
List<CcdiEnterpriseBaseInfo> newRecords = new ArrayList<>();
List<CcdiEnterpriseBaseInfo> updateRecords = new ArrayList<>();
List<IntermediaryEntityImportFailureVO> failures = new ArrayList<>();
// 1. 批量查询已存在的统一社会信用代码
Set<String> existingCreditCodes = getExistingCreditCodes(excelList);
// 2. 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiIntermediaryEntityExcel excel = excelList.get(i);
try {
// 验证数据
validateEntityData(excel, isUpdateSupport, existingCreditCodes);
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
BeanUtils.copyProperties(excel, entity);
// 设置数据来源和审计字段
entity.setDataSource("IMPORT");
entity.setEntSource("INTERMEDIARY");
entity.setCreatedBy(userName);
if (existingCreditCodes.contains(excel.getSocialCreditCode())) {
if (isUpdateSupport) {
// 更新模式:设置更新人
entity.setUpdatedBy(userName);
updateRecords.add(entity);
} else {
throw new RuntimeException("该统一社会信用代码已存在");
}
} else {
newRecords.add(entity);
}
} catch (Exception e) {
IntermediaryEntityImportFailureVO failure = new IntermediaryEntityImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
}
}
// 3. 批量插入新数据
if (!newRecords.isEmpty()) {
saveBatch(newRecords, 500);
}
// 4. 批量更新已有数据(先删除再插入)
if (!updateRecords.isEmpty() && isUpdateSupport) {
// 先批量删除已存在的记录
List<String> creditCodes = updateRecords.stream()
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
.collect(Collectors.toList());
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, creditCodes);
entityMapper.delete(deleteWrapper);
// 批量插入更新后的数据
entityMapper.insertBatch(updateRecords);
}
// 5. 保存失败记录到Redis
if (!failures.isEmpty()) {
String failuresKey = "import:intermediary-entity:" + taskId + ":failures";
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
}
// 6. 更新最终状态
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size() + updateRecords.size());
result.setFailureCount(failures.size());
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
updateImportStatus(taskId, finalStatus, result);
}
@Override
public ImportStatusVO getImportStatus(String taskId) {
String key = "import:intermediary-entity:" + taskId;
Boolean hasKey = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(hasKey)) {
throw new RuntimeException("任务不存在或已过期");
}
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
ImportStatusVO statusVO = new ImportStatusVO();
statusVO.setTaskId((String) statusMap.get("taskId"));
statusVO.setStatus((String) statusMap.get("status"));
statusVO.setTotalCount((Integer) statusMap.get("totalCount"));
statusVO.setSuccessCount((Integer) statusMap.get("successCount"));
statusVO.setFailureCount((Integer) statusMap.get("failureCount"));
statusVO.setProgress((Integer) statusMap.get("progress"));
statusVO.setStartTime((Long) statusMap.get("startTime"));
statusVO.setEndTime((Long) statusMap.get("endTime"));
statusVO.setMessage((String) statusMap.get("message"));
return statusVO;
}
@Override
public List<IntermediaryEntityImportFailureVO> getImportFailures(String taskId) {
String key = "import:intermediary-entity:" + taskId + ":failures";
Object failuresObj = redisTemplate.opsForValue().get(key);
if (failuresObj == null) {
return Collections.emptyList();
}
return JSON.parseArray(JSON.toJSONString(failuresObj), IntermediaryEntityImportFailureVO.class);
}
/**
* 批量查询已存在的统一社会信用代码
*/
private Set<String> getExistingCreditCodes(List<CcdiIntermediaryEntityExcel> excelList) {
List<String> creditCodes = excelList.stream()
.map(CcdiIntermediaryEntityExcel::getSocialCreditCode)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
if (creditCodes.isEmpty()) {
return Collections.emptySet();
}
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, creditCodes);
List<CcdiEnterpriseBaseInfo> existingEntities = entityMapper.selectList(wrapper);
return existingEntities.stream()
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
.collect(Collectors.toSet());
}
/**
* 批量保存
*/
private void saveBatch(List<CcdiEnterpriseBaseInfo> list, int batchSize) {
// 使用真正的批量插入,分批次执行以提高性能
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiEnterpriseBaseInfo> subList = list.subList(i, end);
entityMapper.insertBatch(subList);
}
}
/**
* 更新导入状态
*/
private void updateImportStatus(String taskId, String status, ImportResult result) {
String key = "import:intermediary-entity:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("status", status);
statusData.put("successCount", result.getSuccessCount());
statusData.put("failureCount", result.getFailureCount());
statusData.put("progress", 100);
statusData.put("endTime", System.currentTimeMillis());
if ("SUCCESS".equals(status)) {
statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
} else {
statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "");
}
redisTemplate.opsForHash().putAll(key, statusData);
}
/**
* 验证实体中介数据
*
* @param excel Excel数据
* @param isUpdateSupport 是否支持更新
* @param existingCreditCodes 已存在的统一社会信用代码集合
*/
private void validateEntityData(CcdiIntermediaryEntityExcel excel,
Boolean isUpdateSupport,
Set<String> existingCreditCodes) {
// 验证必填字段:机构名称
if (StringUtils.isEmpty(excel.getEnterpriseName())) {
throw new RuntimeException("机构名称不能为空");
}
// 验证必填字段:统一社会信用代码
if (StringUtils.isEmpty(excel.getSocialCreditCode())) {
throw new RuntimeException("统一社会信用代码不能为空");
}
// 如果统一社会信用代码已存在但未启用更新支持,抛出异常
if (existingCreditCodes.contains(excel.getSocialCreditCode()) && !isUpdateSupport) {
throw new RuntimeException("该统一社会信用代码已存在");
}
// 如果统一社会信用代码不存在,检查唯一性
if (!existingCreditCodes.contains(excel.getSocialCreditCode())) {
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiEnterpriseBaseInfo::getSocialCreditCode, excel.getSocialCreditCode());
if (entityMapper.selectCount(wrapper) > 0) {
throw new RuntimeException("该统一社会信用代码已存在");
}
}
}
}

View File

@@ -0,0 +1,259 @@
package com.ruoyi.ccdi.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.ccdi.domain.CcdiBizIntermediary;
import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryPersonExcel;
import com.ruoyi.ccdi.domain.vo.ImportResult;
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
import com.ruoyi.ccdi.domain.vo.IntermediaryPersonImportFailureVO;
import com.ruoyi.ccdi.mapper.CcdiBizIntermediaryMapper;
import com.ruoyi.ccdi.service.ICcdiIntermediaryPersonImportService;
import com.ruoyi.common.utils.IdCardUtil;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 个人中介异步导入Service实现
*
* @author ruoyi
* @date 2026-02-06
*/
@Service
@EnableAsync
public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediaryPersonImportService {
@Resource
private CcdiBizIntermediaryMapper intermediaryMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
@Async
@Transactional(rollbackFor = Exception.class)
public void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
Boolean isUpdateSupport,
String taskId,
String userName) {
List<CcdiBizIntermediary> newRecords = new ArrayList<>();
List<CcdiBizIntermediary> updateRecords = new ArrayList<>();
List<IntermediaryPersonImportFailureVO> failures = new ArrayList<>();
// 1. 批量查询已存在的证件号
Set<String> existingPersonIds = getExistingPersonIds(excelList);
// 2. 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiIntermediaryPersonExcel excel = excelList.get(i);
try {
// 验证数据
validatePersonData(excel, isUpdateSupport, existingPersonIds);
CcdiBizIntermediary intermediary = new CcdiBizIntermediary();
BeanUtils.copyProperties(excel, intermediary);
// 设置数据来源和审计字段
intermediary.setDataSource("IMPORT");
intermediary.setCreatedBy(userName);
if (existingPersonIds.contains(excel.getPersonId())) {
if (isUpdateSupport) {
// 更新模式:设置更新人
intermediary.setUpdatedBy(userName);
updateRecords.add(intermediary);
} else {
throw new RuntimeException("该证件号码已存在");
}
} else {
newRecords.add(intermediary);
}
} catch (Exception e) {
IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
}
}
// 3. 批量插入新数据
if (!newRecords.isEmpty()) {
saveBatch(newRecords, 500);
}
// 4. 批量更新已有数据(先删除再插入)
if (!updateRecords.isEmpty() && isUpdateSupport) {
// 先批量删除已存在的记录
List<String> personIds = updateRecords.stream()
.map(CcdiBizIntermediary::getPersonId)
.collect(Collectors.toList());
LambdaQueryWrapper<CcdiBizIntermediary> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.in(CcdiBizIntermediary::getPersonId, personIds);
intermediaryMapper.delete(deleteWrapper);
// 批量插入更新后的数据
intermediaryMapper.insertBatch(updateRecords);
}
// 5. 保存失败记录到Redis
if (!failures.isEmpty()) {
String failuresKey = "import:intermediary:" + taskId + ":failures";
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
}
// 6. 更新最终状态
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size() + updateRecords.size());
result.setFailureCount(failures.size());
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
updateImportStatus(taskId, finalStatus, result);
}
@Override
public ImportStatusVO getImportStatus(String taskId) {
String key = "import:intermediary:" + taskId;
Boolean hasKey = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(hasKey)) {
throw new RuntimeException("任务不存在或已过期");
}
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
ImportStatusVO statusVO = new ImportStatusVO();
statusVO.setTaskId((String) statusMap.get("taskId"));
statusVO.setStatus((String) statusMap.get("status"));
statusVO.setTotalCount((Integer) statusMap.get("totalCount"));
statusVO.setSuccessCount((Integer) statusMap.get("successCount"));
statusVO.setFailureCount((Integer) statusMap.get("failureCount"));
statusVO.setProgress((Integer) statusMap.get("progress"));
statusVO.setStartTime((Long) statusMap.get("startTime"));
statusVO.setEndTime((Long) statusMap.get("endTime"));
statusVO.setMessage((String) statusMap.get("message"));
return statusVO;
}
@Override
public List<IntermediaryPersonImportFailureVO> getImportFailures(String taskId) {
String key = "import:intermediary:" + taskId + ":failures";
Object failuresObj = redisTemplate.opsForValue().get(key);
if (failuresObj == null) {
return Collections.emptyList();
}
return JSON.parseArray(JSON.toJSONString(failuresObj), IntermediaryPersonImportFailureVO.class);
}
/**
* 批量查询已存在的证件号
*/
private Set<String> getExistingPersonIds(List<CcdiIntermediaryPersonExcel> excelList) {
List<String> personIds = excelList.stream()
.map(CcdiIntermediaryPersonExcel::getPersonId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
if (personIds.isEmpty()) {
return Collections.emptySet();
}
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
List<CcdiBizIntermediary> existingIntermediaries = intermediaryMapper.selectList(wrapper);
return existingIntermediaries.stream()
.map(CcdiBizIntermediary::getPersonId)
.collect(Collectors.toSet());
}
/**
* 批量保存
*/
private void saveBatch(List<CcdiBizIntermediary> list, int batchSize) {
// 使用真正的批量插入,分批次执行以提高性能
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiBizIntermediary> subList = list.subList(i, end);
intermediaryMapper.insertBatch(subList);
}
}
/**
* 更新导入状态
*/
private void updateImportStatus(String taskId, String status, ImportResult result) {
String key = "import:intermediary:" + taskId;
Map<String, Object> statusData = new HashMap<>();
statusData.put("status", status);
statusData.put("successCount", result.getSuccessCount());
statusData.put("failureCount", result.getFailureCount());
statusData.put("progress", 100);
statusData.put("endTime", System.currentTimeMillis());
if ("SUCCESS".equals(status)) {
statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
} else {
statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "");
}
redisTemplate.opsForHash().putAll(key, statusData);
}
/**
* 验证个人中介数据
*
* @param excel Excel数据
* @param isUpdateSupport 是否支持更新
* @param existingPersonIds 已存在的证件号集合
*/
private void validatePersonData(CcdiIntermediaryPersonExcel excel,
Boolean isUpdateSupport,
Set<String> existingPersonIds) {
// 验证必填字段:姓名
if (StringUtils.isEmpty(excel.getName())) {
throw new RuntimeException("姓名不能为空");
}
// 验证必填字段:证件号码
if (StringUtils.isEmpty(excel.getPersonId())) {
throw new RuntimeException("证件号码不能为空");
}
// 验证证件号码格式
String idCardError = IdCardUtil.getErrorMessage(excel.getPersonId());
if (idCardError != null) {
throw new RuntimeException("证件号码" + idCardError);
}
// 如果证件号已存在但未启用更新支持,抛出异常
if (existingPersonIds.contains(excel.getPersonId()) && !isUpdateSupport) {
throw new RuntimeException("该证件号码已存在");
}
// 如果证件号不存在,检查唯一性
if (!existingPersonIds.contains(excel.getPersonId())) {
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiBizIntermediary::getPersonId, excel.getPersonId());
if (intermediaryMapper.selectCount(wrapper) > 0) {
throw new RuntimeException("该证件号码已存在");
}
}
}
}