diff --git a/doc/plans/2026-02-06-intermediary-async-import.md b/doc/plans/2026-02-06-intermediary-async-import.md new file mode 100644 index 0000000..cda626a --- /dev/null +++ b/doc/plans/2026-02-06-intermediary-async-import.md @@ -0,0 +1,1989 @@ +# 中介库异步导入功能实施计划 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**目标:** 将中介库管理的文件导入功能改造为异步实现,支持个人中介和实体中介两种类型的异步导入 + +**架构:** 采用拆分式设计,为个人中介和实体中介分别创建独立的异步导入服务,使用Spring @Async注解实现异步处理,Redis存储导入状态和失败记录 + +**技术栈:** Spring Boot 3.5.8, MyBatis Plus 3.5.10, Redis, Vue 2.6.12, Element UI + +--- + +## 前置条件 + +**参考资料:** +- 设计文档: `doc/plans/2026-02-06-intermediary-async-import-design.md` +- 员工导入实现: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeImportServiceImpl.java` +- 招聘导入实现: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java` + +**关键依赖:** +- `ImportResultVO` - 导入结果VO(已存在,复用) +- `ImportStatusVO` - 导入状态VO(已存在,复用) +- `AsyncConfig` - 异步配置(已存在,复用) +- `importExecutor` - 导入任务线程池(已存在,复用) + +**Mapper:** +- `CcdiBizIntermediaryMapper` - 个人中介Mapper(已存在) +- `CcdiEnterpriseBaseInfoMapper` - 实体中介Mapper(已存在) + +--- + +## Task 1: 创建个人中介导入失败记录VO + +**文件:** +- Create: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryPersonImportFailureVO.java` + +**Step 1: 创建VO类** + +完整代码: +```java +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; +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryPersonImportFailureVO.java +git commit -m "feat: 添加个人中介导入失败记录VO" +``` + +--- + +## Task 2: 创建实体中介导入失败记录VO + +**文件:** +- Create: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryEntityImportFailureVO.java` + +**Step 1: 创建VO类** + +完整代码: +```java +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; +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryEntityImportFailureVO.java +git commit -m "feat: 添加实体中介导入失败记录VO" +``` + +--- + +## Task 3: 创建个人中介导入Service接口 + +**文件:** +- Create: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java` + +**Step 1: 创建Service接口** + +完整代码: +```java +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 excelList, + Boolean isUpdateSupport, + String taskId, + String userName); + + /** + * 查询导入状态 + * + * @param taskId 任务ID + * @return 导入状态信息 + */ + ImportStatusVO getImportStatus(String taskId); + + /** + * 获取导入失败记录 + * + * @param taskId 任务ID + * @return 失败记录列表 + */ + List getImportFailures(String taskId); +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java +git commit -m "feat: 添加个人中介异步导入Service接口" +``` + +--- + +## Task 4: 创建实体中介导入Service接口 + +**文件:** +- Create: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java` + +**Step 1: 创建Service接口** + +完整代码: +```java +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 excelList, + Boolean isUpdateSupport, + String taskId, + String userName); + + /** + * 查询导入状态 + * + * @param taskId 任务ID + * @return 导入状态信息 + */ + ImportStatusVO getImportStatus(String taskId); + + /** + * 获取导入失败记录 + * + * @param taskId 任务ID + * @return 失败记录列表 + */ + List getImportFailures(String taskId); +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java +git commit -m "feat: 添加实体中介异步导入Service接口" +``` + +--- + +## Task 5: 实现个人中介异步导入Service + +**文件:** +- Create: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java` + +**Step 1: 创建Service实现类** + +完整代码: +```java +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.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 redisTemplate; + + @Override + @Async + @Transactional + public void importPersonAsync(List excelList, + Boolean isUpdateSupport, + String taskId, + String userName) { + List newRecords = new ArrayList<>(); + List updateRecords = new ArrayList<>(); + List failures = new ArrayList<>(); + + // 1. 批量查询已存在的证件号 + Set existingPersonIds = getExistingPersonIds(excelList); + + // 2. 分类数据 + for (CcdiIntermediaryPersonExcel excel : excelList) { + try { + // 验证必填字段 + if (StringUtils.isEmpty(excel.getName())) { + throw new RuntimeException("姓名不能为空"); + } + if (StringUtils.isEmpty(excel.getPersonId())) { + throw new RuntimeException("证件号码不能为空"); + } + + CcdiBizIntermediary person = new CcdiBizIntermediary(); + BeanUtils.copyProperties(excel, person); + person.setPersonType("中介"); + person.setDataSource("IMPORT"); + + if (existingPersonIds.contains(excel.getPersonId())) { + if (isUpdateSupport) { + person.setUpdateBy(userName); + updateRecords.add(person); + } else { + throw new RuntimeException("该证件号已存在"); + } + } else { + person.setCreateBy(userName); + person.setUpdateBy(userName); + newRecords.add(person); + } + } catch (Exception e) { + IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(e.getMessage()); + failures.add(failure); + } + } + + // 3. 批量插入 + if (!newRecords.isEmpty()) { + intermediaryMapper.insertBatch(newRecords); + } + + // 4. 批量更新 + if (!updateRecords.isEmpty()) { + intermediaryMapper.updateBatch(updateRecords); + } + + // 5. 保存失败记录到Redis + if (!failures.isEmpty()) { + String failuresKey = "import:intermediary-person:" + 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-person:" + taskId; + Boolean hasKey = redisTemplate.hasKey(key); + + if (Boolean.FALSE.equals(hasKey)) { + throw new RuntimeException("任务不存在或已过期"); + } + + Map 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 getImportFailures(String taskId) { + String key = "import:intermediary-person:" + taskId + ":failures"; + Object failuresObj = redisTemplate.opsForValue().get(key); + + if (failuresObj == null) { + return Collections.emptyList(); + } + + return JSON.parseArray(JSON.toJSONString(failuresObj), IntermediaryPersonImportFailureVO.class); + } + + /** + * 批量查询已存在的证件号 + */ + private Set getExistingPersonIds(List excelList) { + List personIds = excelList.stream() + .map(CcdiIntermediaryPersonExcel::getPersonId) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); + + if (personIds.isEmpty()) { + return Collections.emptySet(); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(CcdiBizIntermediary::getBizId, CcdiBizIntermediary::getPersonId); + wrapper.in(CcdiBizIntermediary::getPersonId, personIds); + List existingList = intermediaryMapper.selectList(wrapper); + + return existingList.stream() + .map(CcdiBizIntermediary::getPersonId) + .collect(Collectors.toSet()); + } + + /** + * 更新导入状态 + */ + private void updateImportStatus(String taskId, String status, ImportResult result) { + String key = "import:intermediary-person:" + taskId; + Map 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); + } +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java +git commit -m "feat: 实现个人中介异步导入Service" +``` + +--- + +## Task 6: 实现实体中介异步导入Service + +**文件:** +- Create: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java` + +**Step 1: 创建Service实现类** + +完整代码: +```java +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 enterpriseMapper; + + @Resource + private RedisTemplate redisTemplate; + + @Override + @Async + @Transactional + public void importEntityAsync(List excelList, + Boolean isUpdateSupport, + String taskId, + String userName) { + List newRecords = new ArrayList<>(); + List updateRecords = new ArrayList<>(); + List failures = new ArrayList<>(); + + // 1. 批量查询已存在的统一社会信用代码 + Set existingCodes = getExistingSocialCreditCodes(excelList); + + // 2. 分类数据 + for (CcdiIntermediaryEntityExcel excel : excelList) { + try { + // 验证必填字段 + if (StringUtils.isEmpty(excel.getEnterpriseName())) { + throw new RuntimeException("机构名称不能为空"); + } + + CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo(); + BeanUtils.copyProperties(excel, entity); + entity.setRiskLevel("1"); + entity.setEntSource("INTERMEDIARY"); + entity.setDataSource("IMPORT"); + + if (StringUtils.isNotEmpty(excel.getSocialCreditCode()) && + existingCodes.contains(excel.getSocialCreditCode())) { + if (isUpdateSupport) { + entity.setUpdateBy(userName); + updateRecords.add(entity); + } else { + throw new RuntimeException("该统一社会信用代码已存在"); + } + } else { + entity.setCreateBy(userName); + entity.setUpdateBy(userName); + 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()) { + enterpriseMapper.insertBatch(newRecords); + } + + // 4. 批量更新 + if (!updateRecords.isEmpty()) { + enterpriseMapper.updateBatch(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 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 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 getExistingSocialCreditCodes(List excelList) { + List codes = excelList.stream() + .map(CcdiIntermediaryEntityExcel::getSocialCreditCode) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); + + if (codes.isEmpty()) { + return Collections.emptySet(); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(CcdiEnterpriseBaseInfo::getSocialCreditCode); + wrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, codes); + List existingList = enterpriseMapper.selectList(wrapper); + + return existingList.stream() + .map(CcdiEnterpriseBaseInfo::getSocialCreditCode) + .collect(Collectors.toSet()); + } + + /** + * 更新导入状态 + */ + private void updateImportStatus(String taskId, String status, ImportResult result) { + String key = "import:intermediary-entity:" + taskId; + Map 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); + } +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java +git commit -m "feat: 实现实体中介异步导入Service" +``` + +--- + +## Task 7: 修改Controller - 注入Service和添加辅助方法 + +**文件:** +- Modify: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java` + +**Step 1: 添加导入Service注入** + +在类的开头添加: +```java +@Resource +private ICcdiIntermediaryPersonImportService personImportService; + +@Resource +private ICcdiIntermediaryEntityImportService entityImportService; +``` + +**Step 2: 添加初始化导入状态辅助方法** + +在Controller类中添加私有方法: +```java +/** + * 初始化导入状态到Redis + */ +private void initImportStatus(String taskType, String taskId, int totalCount) { + String key = "import:" + taskType + ":" + taskId; + Map statusData = new HashMap<>(); + statusData.put("taskId", taskId); + statusData.put("status", "PROCESSING"); + statusData.put("totalCount", totalCount); + statusData.put("successCount", 0); + statusData.put("failureCount", 0); + statusData.put("progress", 0); + statusData.put("startTime", System.currentTimeMillis()); + statusData.put("message", "正在处理中..."); + + redisTemplate.opsForHash().putAll(key, statusData); + redisTemplate.expire(key, 7, TimeUnit.DAYS); +} +``` + +注意: 需要在Controller中注入`RedisTemplate redisTemplate` + +**Step 3: 添加RedisTemplate注入** + +在类的开头添加: +```java +@Resource +private RedisTemplate redisTemplate; +``` + +**Step 4: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 5: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java +git commit -m "feat: Controller添加导入Service注入和辅助方法" +``` + +--- + +## Task 8: 修改Controller - 改造个人中介导入接口为异步 + +**文件:** +- Modify: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java` + +**Step 1: 修改importPersonData方法** + +将方法修改为: +```java +/** + * 导入个人中介数据(异步) + */ +@Operation(summary = "导入个人中介数据") +@PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')") +@Log(title = "个人中介", businessType = BusinessType.IMPORT) +@PostMapping("/importPersonData") +public AjaxResult importPersonData(MultipartFile file, + @RequestParam(defaultValue = "false") boolean updateSupport) + throws Exception { + List list = EasyExcelUtil.importExcel( + file.getInputStream(), + CcdiIntermediaryPersonExcel.class + ); + + if (list == null || list.isEmpty()) { + return error("至少需要一条数据"); + } + + // 生成任务ID + String taskId = UUID.randomUUID().toString(); + + // 获取当前用户名 + String userName = getUsername(); + + // 初始化导入状态到Redis + initImportStatus("intermediary-person", taskId, list.size()); + + // 提交异步任务 + personImportService.importPersonAsync(list, updateSupport, taskId, userName); + + // 立即返回,不等待后台任务完成 + ImportResultVO result = new ImportResultVO(); + result.setTaskId(taskId); + result.setStatus("PROCESSING"); + result.setMessage("导入任务已提交,正在后台处理"); + + return AjaxResult.success("导入任务已提交,正在后台处理", result); +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java +git commit -m "feat: 改造个人中介导入接口为异步" +``` + +--- + +## Task 9: 修改Controller - 添加个人中介状态查询接口 + +**文件:** +- Modify: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java` + +**Step 1: 添加getPersonImportStatus方法** + +在Controller类中添加: +```java +/** + * 查询个人中介导入状态 + */ +@Operation(summary = "查询个人中介导入状态") +@GetMapping("/importPersonStatus/{taskId}") +public AjaxResult getPersonImportStatus(@PathVariable String taskId) { + try { + ImportStatusVO status = personImportService.getImportStatus(taskId); + return success(status); + } catch (Exception e) { + return error(e.getMessage()); + } +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java +git commit -m "feat: 添加个人中介导入状态查询接口" +``` + +--- + +## Task 10: 修改Controller - 添加个人中介失败记录查询接口 + +**文件:** +- Modify: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java` + +**Step 1: 添加getPersonImportFailures方法** + +在Controller类中添加: +```java +/** + * 查询个人中介导入失败记录 + */ +@Operation(summary = "查询个人中介导入失败记录") +@GetMapping("/importPersonFailures/{taskId}") +public TableDataInfo getPersonImportFailures( + @PathVariable String taskId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + + List failures = + personImportService.getImportFailures(taskId); + + // 手动分页 + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, failures.size()); + + List pageData = failures.subList(fromIndex, toIndex); + + return getDataTable(pageData, failures.size()); +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java +git commit -m "feat: 添加个人中介导入失败记录查询接口" +``` + +--- + +## Task 11: 修改Controller - 改造实体中介导入接口为异步 + +**文件:** +- Modify: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java` + +**Step 1: 修改importEntityData方法** + +将方法修改为: +```java +/** + * 导入实体中介数据(异步) + */ +@Operation(summary = "导入实体中介数据") +@PreAuthorize("@ss.hasPermi('ccdi:intermediary:import')") +@Log(title = "实体中介", businessType = BusinessType.IMPORT) +@PostMapping("/importEntityData") +public AjaxResult importEntityData(MultipartFile file, + @RequestParam(defaultValue = "false") boolean updateSupport) + throws Exception { + List list = EasyExcelUtil.importExcel( + file.getInputStream(), + CcdiIntermediaryEntityExcel.class + ); + + if (list == null || list.isEmpty()) { + return error("至少需要一条数据"); + } + + // 生成任务ID + String taskId = UUID.randomUUID().toString(); + + // 获取当前用户名 + String userName = getUsername(); + + // 初始化导入状态到Redis + initImportStatus("intermediary-entity", taskId, list.size()); + + // 提交异步任务 + entityImportService.importEntityAsync(list, updateSupport, taskId, userName); + + // 立即返回,不等待后台任务完成 + ImportResultVO result = new ImportResultVO(); + result.setTaskId(taskId); + result.setStatus("PROCESSING"); + result.setMessage("导入任务已提交,正在后台处理"); + + return AjaxResult.success("导入任务已提交,正在后台处理", result); +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java +git commit -m "feat: 改造实体中介导入接口为异步" +``` + +--- + +## Task 12: 修改Controller - 添加实体中介状态查询接口 + +**文件:** +- Modify: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java` + +**Step 1: 添加getEntityImportStatus方法** + +在Controller类中添加: +```java +/** + * 查询实体中介导入状态 + */ +@Operation(summary = "查询实体中介导入状态") +@GetMapping("/importEntityStatus/{taskId}") +public AjaxResult getEntityImportStatus(@PathVariable String taskId) { + try { + ImportStatusVO status = entityImportService.getImportStatus(taskId); + return success(status); + } catch (Exception e) { + return error(e.getMessage()); + } +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java +git commit -m "feat: 添加实体中介导入状态查询接口" +``` + +--- + +## Task 13: 修改Controller - 添加实体中介失败记录查询接口 + +**文件:** +- Modify: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java` + +**Step 1: 添加getEntityImportFailures方法** + +在Controller类中添加: +```java +/** + * 查询实体中介导入失败记录 + */ +@Operation(summary = "查询实体中介导入失败记录") +@GetMapping("/importEntityFailures/{taskId}") +public TableDataInfo getEntityImportFailures( + @PathVariable String taskId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + + List failures = + entityImportService.getImportFailures(taskId); + + // 手动分页 + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, failures.size()); + + List pageData = failures.subList(fromIndex, toIndex); + + return getDataTable(pageData, failures.size()); +} +``` + +**Step 2: 编译验证** + +Run: `mvn compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 3: 提交** + +```bash +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java +git commit -m "feat: 添加实体中介导入失败记录查询接口" +``` + +--- + +## Task 14: 前端 - 添加API定义 + +**文件:** +- Modify: `ruoyi-ui/src/api/ccdiIntermediary.js` + +**Step 1: 添加个人中介导入API** + +在文件末尾添加: +```javascript +// 查询个人中介导入状态 +export function getPersonImportStatus(taskId) { + return request({ + url: '/ccdi/intermediary/importPersonStatus/' + taskId, + method: 'get' + }) +} + +// 查询个人中介导入失败记录 +export function getPersonImportFailures(taskId, pageNum, pageSize) { + return request({ + url: '/ccdi/intermediary/importPersonFailures/' + taskId, + method: 'get', + params: { pageNum, pageSize } + }) +} + +// 查询实体中介导入状态 +export function getEntityImportStatus(taskId) { + return request({ + url: '/ccdi/intermediary/importEntityStatus/' + taskId, + method: 'get' + }) +} + +// 查询实体中介导入失败记录 +export function getEntityImportFailures(taskId, pageNum, pageSize) { + return request({ + url: '/ccdi/intermediary/importEntityFailures/' + taskId, + method: 'get', + params: { pageNum, pageSize } + }) +} +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/api/ccdiIntermediary.js +git commit -m "feat: 添加中介导入状态和失败记录查询API" +``` + +--- + +## Task 15: 前端 - 修改Vue组件 - 添加data属性 + +**文件:** +- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue` + +**Step 1: 在data()中添加导入相关属性** + +在`data()`函数的`return`对象中添加: +```javascript +// 个人中介导入相关 +personPollingTimer: null, +personShowFailureButton: false, +personCurrentTaskId: null, +personFailureDialogVisible: false, +personFailureList: [], +personFailureLoading: false, +personFailureTotal: 0, +personFailureQueryParams: { + pageNum: 1, + pageSize: 10 +}, + +// 实体中介导入相关 +entityPollingTimer: null, +entityShowFailureButton: false, +entityCurrentTaskId: null, +entityFailureDialogVisible: false, +entityFailureList: [], +entityFailureLoading: false, +entityFailureTotal: 0, +entityFailureQueryParams: { + pageNum: 1, + pageSize: 10 +} +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiIntermediary/index.vue +git commit -m "feat: 添加中介导入相关data属性" +``` + +--- + +## Task 16: 前端 - 修改handleFileSuccess方法 + +**文件:** +- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue` + +**Step 1: 修改handleFileSuccess方法** + +将`handleFileSuccess`方法修改为: +```javascript +handleFileSuccess(response, file, fileList) { + this.upload.isUploading = false; + this.upload.open = false; + + if (response.code === 200) { + const taskId = response.data.taskId; + const importType = this.upload.importType; // 'person' 或 'entity' + + // 显示后台处理提示 + const typeName = importType === 'person' ? '个人中介' : '实体中介'; + this.$notify({ + title: '导入任务已提交', + message: `${typeName}数据正在后台处理中,处理完成后将通知您`, + type: 'info', + duration: 3000 + }); + + // 根据类型开始轮询 + if (importType === 'person') { + this.startPersonImportPolling(taskId); + } else { + this.startEntityImportPolling(taskId); + } + } else { + this.$modal.msgError(response.msg); + } +} +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiIntermediary/index.vue +git commit -m "feat: 修改handleFileSuccess支持异步导入轮询" +``` + +--- + +## Task 17: 前端 - 添加轮询和完成处理方法 + +**文件:** +- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue` + +**Step 1: 在methods中添加轮询方法** + +在`methods`对象中添加: +```javascript +// 个人中介轮询 +startPersonImportPolling(taskId) { + this.personPollingTimer = setInterval(async () => { + try { + const response = await getPersonImportStatus(taskId); + + if (response.data && response.data.status !== 'PROCESSING') { + clearInterval(this.personPollingTimer); + this.handlePersonImportComplete(response.data); + } + } catch (error) { + clearInterval(this.personPollingTimer); + this.$modal.msgError('查询导入状态失败: ' + error.message); + } + }, 2000); +}, + +// 实体中介轮询 +startEntityImportPolling(taskId) { + this.entityPollingTimer = setInterval(async () => { + try { + const response = await getEntityImportStatus(taskId); + + if (response.data && response.data.status !== 'PROCESSING') { + clearInterval(this.entityPollingTimer); + this.handleEntityImportComplete(response.data); + } + } catch (error) { + clearInterval(this.entityPollingTimer); + this.$modal.msgError('查询导入状态失败: ' + error.message); + } + }, 2000); +}, + +// 个人中介导入完成处理 +handlePersonImportComplete(statusResult) { + if (statusResult.status === 'SUCCESS') { + this.$notify({ + title: '导入完成', + message: `个人中介数据全部成功!共导入${statusResult.totalCount}条`, + type: 'success', + duration: 5000 + }); + this.getList(); + } else if (statusResult.failureCount > 0) { + this.$notify({ + title: '导入完成', + message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`, + type: 'warning', + duration: 5000 + }); + + this.personShowFailureButton = true; + this.personCurrentTaskId = statusResult.taskId; + this.getList(); + } +}, + +// 实体中介导入完成处理 +handleEntityImportComplete(statusResult) { + if (statusResult.status === 'SUCCESS') { + this.$notify({ + title: '导入完成', + message: `实体中介数据全部成功!共导入${statusResult.totalCount}条`, + type: 'success', + duration: 5000 + }); + this.getList(); + } else if (statusResult.failureCount > 0) { + this.$notify({ + title: '导入完成', + message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`, + type: 'warning', + duration: 5000 + }); + + this.entityShowFailureButton = true; + this.entityCurrentTaskId = statusResult.taskId; + this.getList(); + } +} +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiIntermediary/index.vue +git commit -m "feat: 添加中介导入轮询和完成处理方法" +``` + +--- + +## Task 18: 前端 - 添加beforeDestroy生命周期钩子 + +**文件:** +- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue` + +**Step 1: 添加beforeDestroy钩子** + +在组件选项中添加: +```javascript +beforeDestroy() { + // 清除个人中介轮询定时器 + if (this.personPollingTimer) { + clearInterval(this.personPollingTimer); + this.personPollingTimer = null; + } + + // 清除实体中介轮询定时器 + if (this.entityPollingTimer) { + clearInterval(this.entityPollingTimer); + this.entityPollingTimer = null; + } +} +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiIntermediary/index.vue +git commit -m "feat: 添加beforeDestroy生命周期钩子清除定时器" +``` + +--- + +## Task 19: 前端 - 添加失败记录查询方法 + +**文件:** +- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue` + +**Step 1: 在methods中添加查询失败记录方法** + +在`methods`对象中添加: +```javascript +// 查看个人中介导入失败记录 +viewPersonImportFailures() { + this.personFailureDialogVisible = true; + this.getPersonFailureList(); +}, + +// 获取个人中介失败记录列表 +getPersonFailureList() { + this.personFailureLoading = true; + getPersonImportFailures( + this.personCurrentTaskId, + this.personFailureQueryParams.pageNum, + this.personFailureQueryParams.pageSize + ).then(response => { + this.personFailureList = response.rows; + this.personFailureTotal = response.total; + this.personFailureLoading = false; + }).catch(error => { + this.personFailureLoading = false; + this.$modal.msgError('查询失败记录失败: ' + error.message); + }); +}, + +// 查看实体中介导入失败记录 +viewEntityImportFailures() { + this.entityFailureDialogVisible = true; + this.getEntityFailureList(); +}, + +// 获取实体中介失败记录列表 +getEntityFailureList() { + this.entityFailureLoading = true; + getEntityImportFailures( + this.entityCurrentTaskId, + this.entityFailureQueryParams.pageNum, + this.entityFailureQueryParams.pageSize + ).then(response => { + this.entityFailureList = response.rows; + this.entityFailureTotal = response.total; + this.entityFailureLoading = false; + }).catch(error => { + this.entityFailureLoading = false; + this.$modal.msgError('查询失败记录失败: ' + error.message); + }); +} +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiIntermediary/index.vue +git commit -m "feat: 添加中介导入失败记录查询方法" +``` + +--- + +## Task 20: 前端 - 添加失败记录按钮 + +**文件:** +- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue` + +**Step 1: 在template中添加失败记录按钮** + +在按钮区域添加(参考现有的按钮布局): +```vue + + + 查看个人中介导入失败记录 + + + + + 查看实体中介导入失败记录 + +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiIntermediary/index.vue +git commit -m "feat: 添加中介导入失败记录查看按钮" +``` + +--- + +## Task 21: 前端 - 添加个人中介失败记录对话框 + +**文件:** +- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue` + +**Step 1: 在template中添加个人中介失败记录对话框** + +在template末尾添加: +```vue + + + + + + + + + + + + + + + + +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiIntermediary/index.vue +git commit -m "feat: 添加个人中介导入失败记录对话框" +``` + +--- + +## Task 22: 前端 - 添加实体中介失败记录对话框 + +**文件:** +- Modify: `ruoyi-ui/src/views/ccdiIntermediary/index.vue` + +**Step 1: 在template中添加实体中介失败记录对话框** + +在个人中介对话框后面添加: +```vue + + + + + + + + + + + + + + + + +``` + +**Step 2: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiIntermediary/index.vue +git commit -m "feat: 添加实体中介导入失败记录对话框" +``` + +--- + +## Task 23: 更新API文档 + +**文件:** +- Modify: `doc/api/ccdi_intermediary_api.md` + +**Step 1: 添加导入相关接口文档** + +在文档末尾添加: +```markdown +## 导入相关接口 + +### 1. 导入个人中介数据 + +**接口地址:** `/ccdi/intermediary/importPersonData` +**请求方式:** POST +**接口描述:** 异步导入个人中介数据 + +**请求参数:** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | MultipartFile | 是 | Excel文件 | +| updateSupport | boolean | 否 | 是否更新已存在的数据,默认false | + +**响应示例:** +```json +{ + "code": 200, + "msg": "导入任务已提交,正在后台处理", + "data": { + "taskId": "550e8400-e29b-41d4-a716-446655440000", + "status": "PROCESSING", + "message": "导入任务已提交,正在后台处理" + } +} +``` + +### 2. 查询个人中介导入状态 + +**接口地址:** `/ccdi/intermediary/importPersonStatus/{taskId}` +**请求方式:** GET +**接口描述:** 查询个人中介导入状态 + +**路径参数:** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| taskId | String | 是 | 任务ID | + +**响应示例:** +```json +{ + "code": 200, + "data": { + "taskId": "550e8400-e29b-41d4-a716-446655440000", + "status": "SUCCESS", + "totalCount": 100, + "successCount": 98, + "failureCount": 2, + "progress": 100, + "message": "成功98条,失败2条" + } +} +``` + +### 3. 查询个人中介导入失败记录 + +**接口地址:** `/ccdi/intermediary/importPersonFailures/{taskId}` +**请求方式:** GET +**接口描述:** 查询个人中介导入失败记录 + +**路径参数:** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| taskId | String | 是 | 任务ID | + +**Query参数:** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| pageNum | Integer | 否 | 页码,默认1 | +| pageSize | Integer | 否 | 每页大小,默认10 | + +**响应示例:** +```json +{ + "code": 200, + "rows": [ + { + "name": "张三", + "personId": "110101199001011234", + "personType": "中介", + "gender": "男", + "mobile": "13800138000", + "company": "某公司", + "errorMessage": "该证件号已存在" + } + ], + "total": 2 +} +``` + +### 4. 导入实体中介数据 + +**接口地址:** `/ccdi/intermediary/importEntityData` +**请求方式:** POST +**接口描述:** 异步导入实体中介数据 + +**请求参数:** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | MultipartFile | 是 | Excel文件 | +| updateSupport | boolean | 否 | 是否更新已存在的数据,默认false | + +**响应示例:** 同个人中介导入 + +### 5. 查询实体中介导入状态 + +**接口地址:** `/ccdi/intermediary/importEntityStatus/{taskId}` +**请求方式:** GET +**接口描述:** 查询实体中介导入状态 + +**响应示例:** 同个人中介状态查询 + +### 6. 查询实体中介导入失败记录 + +**接口地址:** `/ccdi/intermediary/importEntityFailures/{taskId}` +**请求方式:** GET +**接口描述:** 查询实体中介导入失败记录 + +**响应示例:** +```json +{ + "code": 200, + "rows": [ + { + "enterpriseName": "某机构", + "socialCreditCode": "91110000123456789X", + "enterpriseType": "企业", + "enterpriseNature": "民营企业", + "legalRepresentative": "张三", + "establishDate": "2020-01-01", + "errorMessage": "该统一社会信用代码已存在" + } + ], + "total": 1 +} +``` + +## 状态枚举 + +| 状态值 | 说明 | +|--------|------| +| PROCESSING | 处理中 | +| SUCCESS | 全部成功 | +| PARTIAL_SUCCESS | 部分成功 | +| FAILED | 全部失败 | +``` + +**Step 2: 提交** + +```bash +git add doc/api/ccdi_intermediary_api.md +git commit -m "docs: 添加中介导入相关接口文档" +``` + +--- + +## Task 24: 创建测试脚本 + +**文件:** +- Create: `test/test_intermediary_import.py` + +**Step 1: 创建测试脚本** + +完整代码: +```python +import requests +import json +import time +import os + +# 配置 +BASE_URL = "http://localhost:8080" +USERNAME = "admin" +PASSWORD = "admin123" + +def login(): + """登录获取token""" + url = f"{BASE_URL}/login/test" + data = { + "username": USERNAME, + "password": PASSWORD + } + response = requests.post(url, data=data) + result = response.json() + if result.get("code") == 200: + print("✓ 登录成功") + return result.get("token") + else: + print(f"✗ 登录失败: {result.get('msg')}") + return None + +def test_person_import(file_path, update_support=False): + """测试个人中介导入""" + print("\n=== 测试个人中介导入 ===") + url = f"{BASE_URL}/ccdi/intermediary/importPersonData" + headers = {"Authorization": f"Bearer {TOKEN}"} + + with open(file_path, 'rb') as f: + files = {'file': f} + data = {'updateSupport': update_support} + response = requests.post(url, headers=headers, files=files, data=data) + + result = response.json() + if result.get("code") == 200: + task_id = result.get("data").get("taskId") + print(f"✓ 个人中介导入任务已提交, taskId: {task_id}") + return task_id + else: + print(f"✗ 个人中介导入失败: {result.get('msg')}") + return None + +def test_entity_import(file_path, update_support=False): + """测试实体中介导入""" + print("\n=== 测试实体中介导入 ===") + url = f"{BASE_URL}/ccdi/intermediary/importEntityData" + headers = {"Authorization": f"Bearer {TOKEN}"} + + with open(file_path, 'rb') as f: + files = {'file': f} + data = {'updateSupport': update_support} + response = requests.post(url, headers=headers, files=files, data=data) + + result = response.json() + if result.get("code") == 200: + task_id = result.get("data").get("taskId") + print(f"✓ 实体中介导入任务已提交, taskId: {task_id}") + return task_id + else: + print(f"✗ 实体中介导入失败: {result.get('msg')}") + return None + +def wait_for_completion(task_id, import_type="person"): + """等待导入完成""" + print(f"\n=== 等待{import_type}中介导入完成 ===") + url = f"{BASE_URL}/ccdi/intermediary/import{import_type.capitalize()}Status/{task_id}" + headers = {"Authorization": f"Bearer {TOKEN}"} + + max_retries = 30 + for i in range(max_retries): + response = requests.get(url, headers=headers) + result = response.json() + + if result.get("code") == 200: + status_data = result.get("data") + status = status_data.get("status") + print(f"状态: {status}, 进度: {status_data.get('progress')}%, " + f"成功: {status_data.get('successCount')}, 失败: {status_data.get('failureCount')}") + + if status != "PROCESSING": + print(f"✓ 导入完成: {status_data.get('message')}") + return status_data + + time.sleep(2) + + print("✗ 导入超时") + return None + +def get_failures(task_id, import_type="person"): + """获取失败记录""" + print(f"\n=== 获取{import_type}中介导入失败记录 ===") + url = f"{BASE_URL}/ccdi/intermediary/import{import_type.capitalize()}Failures/{task_id}" + headers = {"Authorization": f"Bearer {TOKEN}"} + params = {"pageNum": 1, "pageSize": 10} + + response = requests.get(url, headers=headers, params=params) + result = response.json() + + if result.get("code") == 200: + rows = result.get("rows", []) + total = result.get("total", 0) + print(f"✓ 共有 {total} 条失败记录") + for i, row in enumerate(rows[:5], 1): + print(f" {i}. {row.get('errorMessage')}") + if total > 5: + print(f" ... 还有 {total - 5} 条") + else: + print(f"✗ 获取失败记录失败: {result.get('msg')}") + +if __name__ == "__main__": + # 登录 + TOKEN = login() + if not TOKEN: + exit(1) + + # 测试个人中介导入 + person_file = "doc/test-data/intermediary/intermediary_person_test.xlsx" + if os.path.exists(person_file): + task_id = test_person_import(person_file) + if task_id: + status = wait_for_completion(task_id, "person") + if status and status.get("failureCount") > 0: + get_failures(task_id, "person") + + # 测试实体中介导入 + entity_file = "doc/test-data/intermediary/intermediary_entity_test.xlsx" + if os.path.exists(entity_file): + task_id = test_entity_import(entity_file) + if task_id: + status = wait_for_completion(task_id, "entity") + if status and status.get("failureCount") > 0: + get_failures(task_id, "entity") + + print("\n=== 测试完成 ===") +``` + +**Step 2: 提交** + +```bash +git add test/test_intermediary_import.py +git commit -m "test: 添加中介导入测试脚本" +``` + +--- + +## Task 25: 最终编译和测试 + +**Step 1: 编译后端** + +Run: `mvn clean compile -pl ruoyi-ccdi` +Expected: BUILD SUCCESS + +**Step 2: 检查前端语法** + +Run: `cd ruoyi-ui && npm run lint -- --no-fix` +Expected: No ESLint errors + +**Step 3: 最终提交** + +```bash +git add . +git commit -m "feat: 完成中介库异步导入功能实现" +``` + +--- + +## 测试检查清单 + +### 功能测试 + +- [ ] 个人中介导入 - 全部成功 +- [ ] 个人中介导入 - 部分失败 +- [ ] 个人中介导入 - 重复数据不更新 +- [ ] 个人中介导入 - 重复数据更新 +- [ ] 实体中介导入 - 全部成功 +- [ ] 实体中介导入 - 部分失败 +- [ ] 实体中介导入 - 重复数据不更新 +- [ ] 实体中介导入 - 重复数据更新 +- [ ] 状态查询 - 返回正确状态 +- [ ] 失败记录查询 - 正确显示 +- [ ] 前端轮询 - 每2秒查询一次 +- [ ] 前端通知 - 显示正确的成功/警告通知 +- [ ] 失败记录UI - 个人和实体按钮正确显示 +- [ ] 失败记录对话框 - 正确展示失败数据 + +### 性能测试 + +- [ ] 导入接口响应时间 < 500ms +- [ ] 500条数据处理时间 < 5秒 +- [ ] Redis数据正确存储,TTL为7天 + +### 异常测试 + +- [ ] 空文件上传 +- [ ] 不存在的taskId查询 +- [ ] 并发导入多个文件 + +--- + +## 预期完成时间 + +- 后端开发: 2-3小时 +- 前端开发: 1-2小时 +- 测试和调试: 1-2小时 + +**总计: 4-7小时** + +--- + +**实施计划版本:** 1.0 +**创建日期:** 2026-02-06 +**创建人员:** Claude