diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java index 53b80c1..f59fdf6 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java @@ -2,8 +2,10 @@ package com.ruoyi.ccdi.project.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.project.domain.dto.CcdiFileUploadQueryDTO; +import com.ruoyi.ccdi.project.domain.dto.CcdiPullBankInfoSubmitDTO; import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord; import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiIdCardParseVO; import com.ruoyi.ccdi.project.service.ICcdiFileUploadService; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; @@ -15,9 +17,12 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.util.List; import java.util.concurrent.RejectedExecutionException; /** @@ -85,6 +90,48 @@ public class CcdiFileUploadController extends BaseController { } } + /** + * 解析身份证文件 + */ + @PostMapping("/parse-id-card-file") + @Operation(summary = "解析身份证文件", description = "解析首个sheet第一列的身份证号") + public AjaxResult parseIdCardFile(@RequestParam MultipartFile file) { + if (file == null || file.isEmpty()) { + return AjaxResult.error("身份证文件不能为空"); + } + List idCards = fileUploadService.parseIdCardFile(file); + return AjaxResult.success("解析成功", new CcdiIdCardParseVO(idCards, idCards.size())); + } + + /** + * 提交拉取本行信息任务 + */ + @PostMapping("/pull-bank-info") + @Operation(summary = "拉取本行信息", description = "按身份证号批量提交拉取本行信息任务") + public AjaxResult pullBankInfo(@RequestBody CcdiPullBankInfoSubmitDTO dto) { + if (dto == null || dto.getProjectId() == null) { + return AjaxResult.error("项目ID不能为空"); + } + if (CollectionUtils.isEmpty(dto.getIdCards())) { + return AjaxResult.error("身份证号不能为空"); + } + if (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate())) { + return AjaxResult.error("开始日期和结束日期不能为空"); + } + + Long userId = SecurityUtils.getUserId(); + String username = SecurityUtils.getUsername(); + String batchId = fileUploadService.submitPullBankInfo( + dto.getProjectId(), + dto.getIdCards(), + dto.getStartDate(), + dto.getEndDate(), + userId, + username + ); + return AjaxResult.success("拉取任务已提交", batchId); + } + /** * 查询上传记录列表 */ diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiPullBankInfoSubmitDTO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiPullBankInfoSubmitDTO.java new file mode 100644 index 0000000..d203921 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiPullBankInfoSubmitDTO.java @@ -0,0 +1,24 @@ +package com.ruoyi.ccdi.project.domain.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 拉取本行信息提交参数 + */ +@Data +public class CcdiPullBankInfoSubmitDTO { + + /** 项目ID */ + private Long projectId; + + /** 身份证号列表 */ + private List idCards; + + /** 开始日期 */ + private String startDate; + + /** 结束日期 */ + private String endDate; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiIdCardExcelRow.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiIdCardExcelRow.java new file mode 100644 index 0000000..899c18d --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiIdCardExcelRow.java @@ -0,0 +1,14 @@ +package com.ruoyi.ccdi.project.domain.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 身份证导入行 + */ +@Data +public class CcdiIdCardExcelRow { + + @ExcelProperty(index = 0) + private String idCard; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiIdCardParseVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiIdCardParseVO.java new file mode 100644 index 0000000..d731f1b --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiIdCardParseVO.java @@ -0,0 +1,22 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 身份证文件解析结果 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CcdiIdCardParseVO { + + /** 解析到的身份证列表 */ + private List idCards; + + /** 数量 */ + private Integer count; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java index a378da2..104cd14 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java @@ -6,6 +6,8 @@ import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord; import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + /** * 文件上传服务接口 * @@ -24,6 +26,32 @@ public interface ICcdiFileUploadService { */ String batchUploadFiles(Long projectId, MultipartFile[] files, String username); + /** + * 解析身份证文件 + * + * @param file Excel 文件 + * @return 身份证号列表 + */ + List parseIdCardFile(MultipartFile file); + + /** + * 提交拉取本行信息任务 + * + * @param projectId 项目ID + * @param idCards 身份证号列表 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param userId 当前登录用户ID + * @param username 当前登录用户名 + * @return 批次ID + */ + String submitPullBankInfo(Long projectId, + List idCards, + String startDate, + String endDate, + Long userId, + String username); + /** * 查询上传记录列表 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java index 7185681..d5d4d56 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java @@ -1,17 +1,20 @@ package com.ruoyi.ccdi.project.service.impl; +import com.alibaba.excel.EasyExcel; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.dto.CcdiFileUploadQueryDTO; import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement; import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord; +import com.ruoyi.ccdi.project.domain.excel.CcdiIdCardExcelRow; import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO; import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper; import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.ccdi.project.service.ICcdiFileUploadService; import com.ruoyi.lsfx.client.LsfxAnalysisClient; +import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest; import com.ruoyi.lsfx.domain.request.GetBankStatementRequest; import com.ruoyi.lsfx.domain.request.GetFileUploadStatusRequest; import com.ruoyi.lsfx.domain.response.*; @@ -34,10 +37,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; +import java.util.regex.Pattern; /** * 文件上传服务实现 @@ -50,6 +56,8 @@ import java.util.concurrent.RejectedExecutionException; public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { private static final int MAX_ERROR_MESSAGE_LENGTH = 2000; + private static final Pattern ID_CARD_PATTERN = + Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])([0-2]\\d|3[01])\\d{3}[0-9Xx]$"); @Data private static class FetchBankStatementResult { @@ -88,6 +96,116 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { return uploadPath + File.separator + "temp"; } + @Override + public List parseIdCardFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new RuntimeException("身份证文件不能为空"); + } + + try { + List rows = EasyExcel.read(file.getInputStream()) + .head(CcdiIdCardExcelRow.class) + .sheet(0) + .headRowNumber(1) + .doReadSync(); + + LinkedHashSet idCards = new LinkedHashSet<>(); + for (CcdiIdCardExcelRow row : rows) { + if (row == null) { + continue; + } + String idCard = row.getIdCard(); + if (!StringUtils.hasText(idCard)) { + continue; + } + String normalized = idCard.trim(); + if (!ID_CARD_PATTERN.matcher(normalized).matches()) { + continue; + } + idCards.add(normalized); + } + + if (idCards.isEmpty()) { + throw new RuntimeException("首个sheet第一列未解析到有效身份证号"); + } + return new ArrayList<>(idCards); + } catch (IOException e) { + throw new RuntimeException("读取身份证文件失败: " + e.getMessage(), e); + } + } + + @Transactional + @Override + public String submitPullBankInfo(Long projectId, + List idCards, + String startDate, + String endDate, + Long userId, + String username) { + if (projectId == null) { + throw new IllegalArgumentException("项目ID不能为空"); + } + if (!StringUtils.hasText(startDate) || !StringUtils.hasText(endDate)) { + throw new IllegalArgumentException("开始日期和结束日期不能为空"); + } + if (idCards == null || idCards.isEmpty()) { + throw new IllegalArgumentException("身份证号不能为空"); + } + + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + if (start.isAfter(end)) { + throw new IllegalArgumentException("开始日期不能晚于结束日期"); + } + + CcdiProject project = projectMapper.selectById(projectId); + if (project == null) { + throw new IllegalArgumentException("项目不存在: projectId=" + projectId); + } + Integer lsfxProjectId = project.getLsfxProjectId(); + if (lsfxProjectId == null) { + throw new IllegalStateException("项目未关联流水分析平台: projectId=" + projectId); + } + + String batchId = UUID.randomUUID().toString().replace("-", ""); + Date now = new Date(); + List records = new ArrayList<>(); + List normalizedIdCards = new ArrayList<>(); + for (String idCard : idCards) { + if (!StringUtils.hasText(idCard)) { + continue; + } + String normalized = idCard.trim(); + normalizedIdCards.add(normalized); + + CcdiFileUploadRecord record = new CcdiFileUploadRecord(); + record.setProjectId(projectId); + record.setLsfxProjectId(lsfxProjectId); + record.setFileName(normalized); + record.setFileSize(0L); + record.setFileStatus("uploading"); + record.setAccountNos(normalized); + record.setUploadTime(now); + record.setUploadUser(username); + records.add(record); + } + if (records.isEmpty()) { + throw new IllegalArgumentException("身份证号不能为空"); + } + + recordMapper.insertBatch(records); + + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + CompletableFuture.runAsync(() -> submitPullBankInfoTasks( + projectId, lsfxProjectId, records, normalizedIdCards, startDate, endDate, userId, batchId + )); + } + }); + return batchId; + } + @Override public Page selectPage(Page page, CcdiFileUploadQueryDTO queryDTO) { @@ -351,6 +469,90 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { statement.setLeAccountNo(trimAccountNo(statement.getLeAccountNo())); } + private void submitPullBankInfoTasks(Long projectId, + Integer lsfxProjectId, + List records, + List idCards, + String startDate, + String endDate, + Long userId, + String batchId) { + log.info("【拉取本行信息】调度线程启动: projectId={}, batchId={}", projectId, batchId); + + for (int i = 0; i < records.size(); i++) { + if (Thread.currentThread().isInterrupted()) { + log.warn("【拉取本行信息】调度线程被中断,停止提交剩余任务"); + break; + } + + CcdiFileUploadRecord record = records.get(i); + String idCard = idCards.get(i); + boolean submitted = false; + int retryCount = 0; + + while (!submitted && retryCount < 2) { + try { + CompletableFuture.runAsync( + () -> processPullBankInfoAsync(projectId, lsfxProjectId, record, idCard, startDate, endDate, userId), + fileUploadExecutor + ); + submitted = true; + log.info("【拉取本行信息】任务提交成功: idCard={}, recordId={}", idCard, record.getId()); + } catch (RejectedExecutionException e) { + retryCount++; + if (retryCount == 1) { + log.warn("【拉取本行信息】线程池已满,等待30秒后重试: idCard={}", idCard); + try { + Thread.sleep(30000); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + log.error("【拉取本行信息】等待被中断: idCard={}", idCard); + updateRecordStatus(record.getId(), "parsed_failed", "任务提交被中断"); + break; + } + } else { + log.error("【拉取本行信息】重试失败,放弃任务: idCard={}", idCard); + updateRecordStatus(record.getId(), "parsed_failed", "系统繁忙,请稍后重试"); + } + } + } + } + } + + public void processPullBankInfoAsync(Long projectId, + Integer lsfxProjectId, + CcdiFileUploadRecord record, + String idCard, + String startDate, + String endDate, + Long userId) { + try { + FetchInnerFlowRequest request = new FetchInnerFlowRequest(); + request.setGroupId(lsfxProjectId); + request.setCustomerNo(idCard); + request.setDataChannelCode("ZJRCU"); + request.setRequestDateId(Integer.parseInt(LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE))); + request.setDataStartDateId(Integer.parseInt(startDate.replace("-", ""))); + request.setDataEndDateId(Integer.parseInt(endDate.replace("-", ""))); + request.setUploadUserId(toUploadUserId(userId)); + + FetchInnerFlowResponse response = lsfxClient.fetchInnerFlow(request); + if (response == null || response.getData() == null || response.getData().isEmpty()) { + throw new RuntimeException("拉取本行信息失败: 未返回logId"); + } + + Integer logId = response.getData().get(0); + if (logId == null) { + throw new RuntimeException("拉取本行信息失败: 未返回logId"); + } + + processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId); + } catch (Exception e) { + log.error("【拉取本行信息】处理失败: idCard={}, recordId={}", idCard, record.getId(), e); + updateFailedRecord(record, e.getMessage()); + } + } + /** * 异步处理单个文件的完整流程 * 包含:上传 → 轮询解析状态 → 获取结果 → 保存流水数据 @@ -399,84 +601,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { } log.info("【文件上传】文件上传成功: logId={}", logId); - - // 步骤3:更新状态为 parsing - log.info("【文件上传】步骤3: 更新状态为解析中, logId={}", logId); - record.setLogId(logId); - record.setFileStatus("parsing"); - recordMapper.updateById(record); - - // 步骤4:轮询解析状态(最多300次,间隔2秒) - log.info("【文件上传】步骤4: 开始轮询解析状态"); - boolean parsingComplete = waitForParsingComplete(lsfxProjectId, logId.toString()); - - if (!parsingComplete) { - throw new RuntimeException("解析超时(超过10分钟),请检查文件格式是否正确"); - } - - // 步骤5:获取文件上传状态 - log.info("【文件上传】步骤5: 获取文件上传状态: logId={}", logId); - - GetFileUploadStatusRequest statusRequest = new GetFileUploadStatusRequest(); - statusRequest.setGroupId(lsfxProjectId); - statusRequest.setLogId(logId); - - GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(statusRequest); - - if (statusResponse == null || statusResponse.getData() == null - || statusResponse.getData().getLogs() == null - || statusResponse.getData().getLogs().isEmpty()) { - throw new RuntimeException("获取文件上传状态失败: 响应数据为空"); - } - - // 获取第一个log项(因为我们传了logId,应该只返回一个) - GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0); - Integer status = logItem.getStatus(); - String uploadStatusDesc = logItem.getUploadStatusDesc(); - - log.info("【文件上传】文件状态: status={}, uploadStatusDesc={}", status, uploadStatusDesc); - - // 步骤6:判断解析结果 - // status=-5 且 uploadStatusDesc="data.wait.confirm.newaccount" 表示解析成功 - boolean parseSuccess = status != null && status == -5 - && "data.wait.confirm.newaccount".equals(uploadStatusDesc); - - if (parseSuccess) { - // 解析成功 - log.info("【文件上传】步骤6: 解析成功,保存主体信息"); - - // 提取主体名称和账号 - List enterpriseNames = logItem.getEnterpriseNameList(); - List accountNos = logItem.getAccountNoList(); - - String enterpriseNamesStr = enterpriseNames != null ? String.join(",", enterpriseNames) : ""; - String accountNosStr = accountNos != null ? String.join(",", accountNos) : ""; - - - log.info("【文件上传】主体信息已保存: enterpriseNames={}, accountNos={}", - enterpriseNamesStr, accountNosStr); - - // 步骤7:获取流水数据并保存 - log.info("【文件上传】步骤7: 获取流水数据"); - FetchBankStatementResult fetchResult = - fetchAndSaveBankStatements(projectId, lsfxProjectId, logId); - if (!fetchResult.isSuccess()) { - updateFailedRecord(record, fetchResult.getErrorMessage()); - return; - } - - record.setFileStatus("parsed_success"); - record.setEnterpriseNames(enterpriseNamesStr); - record.setAccountNos(accountNosStr); - record.setErrorMessage(null); - recordMapper.updateById(record); - - } else { - // 解析失败 - log.warn("【文件上传】步骤6: 解析失败: status={}, desc={}", status, uploadStatusDesc); - updateFailedRecord(record, "解析失败: " + uploadStatusDesc); - } - + processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId); log.info("【文件上传】处理完成: fileName={}", record.getFileName()); } catch (Exception e) { @@ -496,6 +621,74 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { } } + private void processRecordAfterLogIdReady(Long projectId, + Integer lsfxProjectId, + CcdiFileUploadRecord record, + Integer logId) { + log.info("【文件上传】步骤3: 更新状态为解析中, logId={}", logId); + record.setLogId(logId); + record.setFileStatus("parsing"); + recordMapper.updateById(record); + + log.info("【文件上传】步骤4: 开始轮询解析状态"); + boolean parsingComplete = waitForParsingComplete(lsfxProjectId, logId.toString()); + if (!parsingComplete) { + throw new RuntimeException("解析超时(超过10分钟),请检查文件格式是否正确"); + } + + log.info("【文件上传】步骤5: 获取文件上传状态: logId={}", logId); + GetFileUploadStatusRequest statusRequest = new GetFileUploadStatusRequest(); + statusRequest.setGroupId(lsfxProjectId); + statusRequest.setLogId(logId); + + GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(statusRequest); + if (statusResponse == null || statusResponse.getData() == null + || statusResponse.getData().getLogs() == null + || statusResponse.getData().getLogs().isEmpty()) { + throw new RuntimeException("获取文件上传状态失败: 响应数据为空"); + } + + GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0); + Integer status = logItem.getStatus(); + String uploadStatusDesc = logItem.getUploadStatusDesc(); + String fileName = StringUtils.hasText(logItem.getUploadFileName()) + ? logItem.getUploadFileName() + : logItem.getDownloadFileName(); + if (StringUtils.hasText(fileName)) { + record.setFileName(fileName); + } + + log.info("【文件上传】文件状态: status={}, uploadStatusDesc={}", status, uploadStatusDesc); + boolean parseSuccess = status != null && status == -5 + && "data.wait.confirm.newaccount".equals(uploadStatusDesc); + if (!parseSuccess) { + log.warn("【文件上传】步骤6: 解析失败: status={}, desc={}", status, uploadStatusDesc); + updateFailedRecord(record, "解析失败: " + uploadStatusDesc); + return; + } + + log.info("【文件上传】步骤6: 解析成功,保存主体信息"); + List enterpriseNames = logItem.getEnterpriseNameList(); + List accountNos = logItem.getAccountNoList(); + String enterpriseNamesStr = enterpriseNames != null ? String.join(",", enterpriseNames) : ""; + String accountNosStr = accountNos != null ? String.join(",", accountNos) : ""; + log.info("【文件上传】主体信息已保存: enterpriseNames={}, accountNos={}", + enterpriseNamesStr, accountNosStr); + + log.info("【文件上传】步骤7: 获取流水数据"); + FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId); + if (!fetchResult.isSuccess()) { + updateFailedRecord(record, fetchResult.getErrorMessage()); + return; + } + + record.setFileStatus("parsed_success"); + record.setEnterpriseNames(enterpriseNamesStr); + record.setAccountNos(accountNosStr); + record.setErrorMessage(null); + recordMapper.updateById(record); + } + /** * 轮询解析状态(固定间隔2秒,最多300次) * @@ -665,4 +858,11 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { private void cleanupBankStatements(Long projectId, Integer logId) { bankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId); } + + private Integer toUploadUserId(Long userId) { + if (userId == null) { + throw new IllegalArgumentException("当前登录用户ID不能为空"); + } + return Math.toIntExact(userId); + } } diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml index 7190245..2177c0f 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml @@ -30,13 +30,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" insert into ccdi_file_upload_record ( project_id, lsfx_project_id, file_name, file_size, file_status, - upload_time, upload_user + enterprise_names, account_nos, upload_time, upload_user ) values ( #{item.projectId}, #{item.lsfxProjectId}, #{item.fileName}, - #{item.fileSize}, #{item.fileStatus}, #{item.uploadTime}, - #{item.uploadUser} + #{item.fileSize}, #{item.fileStatus}, #{item.enterpriseNames}, + #{item.accountNos}, #{item.uploadTime}, #{item.uploadUser} ) diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java new file mode 100644 index 0000000..bdbcc32 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java @@ -0,0 +1,67 @@ +package com.ruoyi.ccdi.project.controller; + +import com.ruoyi.ccdi.project.domain.dto.CcdiPullBankInfoSubmitDTO; +import com.ruoyi.ccdi.project.service.ICcdiFileUploadService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.SecurityUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiFileUploadControllerTest { + + private static final Long PROJECT_ID = 100L; + + @InjectMocks + private CcdiFileUploadController controller; + + @Mock + private ICcdiFileUploadService fileUploadService; + + @Test + void parseIdCardFile_shouldReturnAjaxResultSuccess() { + MockMultipartFile file = new MockMultipartFile( + "file", + "ids.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "test".getBytes(StandardCharsets.UTF_8) + ); + when(fileUploadService.parseIdCardFile(file)).thenReturn(List.of("110101199001018888")); + + AjaxResult result = controller.parseIdCardFile(file); + + assertEquals(200, result.get("code")); + } + + @Test + void pullBankInfo_shouldUseCurrentLoginUserInfo() { + CcdiPullBankInfoSubmitDTO dto = new CcdiPullBankInfoSubmitDTO(); + dto.setProjectId(PROJECT_ID); + dto.setIdCards(List.of("110101199001018888")); + dto.setStartDate("2026-03-01"); + dto.setEndDate("2026-03-10"); + + try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { + mocked.when(SecurityUtils::getUserId).thenReturn(9527L); + mocked.when(SecurityUtils::getUsername).thenReturn("admin"); + when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "2026-03-01", "2026-03-10", 9527L, "admin")) + .thenReturn("batch-1"); + + AjaxResult result = controller.pullBankInfo(dto); + + assertEquals(200, result.get("code")); + } + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java index 986b112..63cab47 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java @@ -3,6 +3,8 @@ package com.ruoyi.ccdi.project.service.impl; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; +import com.alibaba.excel.EasyExcel; +import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord; import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper; import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper; @@ -10,6 +12,7 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.lsfx.client.LsfxAnalysisClient; import com.ruoyi.lsfx.domain.request.GetBankStatementRequest; import com.ruoyi.lsfx.domain.response.CheckParseStatusResponse; +import com.ruoyi.lsfx.domain.response.FetchInnerFlowResponse; import com.ruoyi.lsfx.domain.response.GetBankStatementResponse; import com.ruoyi.lsfx.domain.response.GetFileUploadStatusResponse; import com.ruoyi.lsfx.domain.response.UploadFileResponse; @@ -20,8 +23,12 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -31,10 +38,13 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -72,6 +82,68 @@ class CcdiFileUploadServiceImplTest { @TempDir Path tempDir; + @Test + void parseIdCardFile_shouldReadFirstSheetFirstColumnAndDeduplicate() throws Exception { + MultipartFile file = createIdCardExcel( + "身份证号", + "110101199001018888", + "", + "110101199001018888", + "110101199001019999" + ); + + List result = service.parseIdCardFile(file); + + assertEquals(List.of("110101199001018888", "110101199001019999"), result); + } + + @Test + void parseIdCardFile_shouldRejectInvalidIdCard() throws Exception { + MultipartFile file = createIdCardExcel("身份证号", "123456"); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> service.parseIdCardFile(file)); + + assertTrue(exception.getMessage().contains("身份证")); + } + + @Test + void submitPullBankInfo_shouldInsertUploadingRecordsWithIdCardAsAccountNo() { + CcdiProject project = new CcdiProject(); + project.setProjectId(PROJECT_ID); + project.setLsfxProjectId(LSFX_PROJECT_ID); + when(projectMapper.selectById(PROJECT_ID)).thenReturn(project); + + AtomicReference> inserted = new AtomicReference<>(); + doAnswer(invocation -> { + List records = invocation.getArgument(0); + for (int i = 0; i < records.size(); i++) { + records.get(i).setId((long) (i + 1)); + } + inserted.set(new ArrayList<>(records)); + return records.size(); + }).when(recordMapper).insertBatch(any()); + + TransactionSynchronizationManager.initSynchronization(); + try { + String batchId = service.submitPullBankInfo( + PROJECT_ID, + List.of("110101199001018888", "110101199001019999"), + "2026-03-01", + "2026-03-10", + 9527L, + "admin" + ); + + assertNotNull(batchId); + assertEquals("110101199001018888", inserted.get().get(0).getAccountNos()); + assertEquals("admin", inserted.get().get(0).getUploadUser()); + assertEquals("uploading", inserted.get().get(0).getFileStatus()); + assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size()); + } finally { + TransactionSynchronizationManager.clearSynchronization(); + } + } + @Test void submitTasksAsync_shouldNotCreateLocalBatchLogFiles() throws Exception { setField("uploadPath", tempDir.toString()); @@ -151,6 +223,51 @@ class CcdiFileUploadServiceImplTest { verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID); } + @Test + void processPullBankInfoAsync_shouldUpdateFileNameFromStatusResponse() { + when(lsfxClient.fetchInnerFlow(any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID)); + when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID))) + .thenReturn(buildCheckParseStatusResponse(false)); + when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse("XX身份证.xlsx")); + when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class))) + .thenReturn(buildEmptyBankStatementResponse()); + + CcdiFileUploadRecord record = buildRecord(); + service.processPullBankInfoAsync( + PROJECT_ID, + LSFX_PROJECT_ID, + record, + "110101199001018888", + "2026-03-01", + "2026-03-10", + 9527L + ); + + verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(org.mockito.ArgumentMatchers.argThat(item -> + "XX身份证.xlsx".equals(item.getFileName()))); + verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(org.mockito.ArgumentMatchers.argThat(item -> + "parsed_success".equals(item.getFileStatus()))); + } + + @Test + void processPullBankInfoAsync_shouldMarkParsedFailedWhenFetchInnerFlowThrows() { + when(lsfxClient.fetchInnerFlow(any())).thenThrow(new RuntimeException("fetch inner flow failed")); + + CcdiFileUploadRecord record = buildRecord(); + service.processPullBankInfoAsync( + PROJECT_ID, + LSFX_PROJECT_ID, + record, + "110101199001018888", + "2026-03-01", + "2026-03-10", + 9527L + ); + + verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(org.mockito.ArgumentMatchers.argThat(item -> + "parsed_failed".equals(item.getFileStatus()))); + } + @Test void processFileAsync_shouldFailWhenPagedFetchThrows() throws IOException { List events = new ArrayList<>(); @@ -364,6 +481,18 @@ class CcdiFileUploadServiceImplTest { return response; } + private FetchInnerFlowResponse buildFetchInnerFlowResponse(Integer logId) { + FetchInnerFlowResponse response = new FetchInnerFlowResponse(); + response.setData(List.of(logId)); + return response; + } + + private GetFileUploadStatusResponse buildParsedSuccessStatusResponse(String uploadFileName) { + GetFileUploadStatusResponse response = buildParsedSuccessStatusResponse(); + response.getData().getLogs().get(0).setUploadFileName(uploadFileName); + return response; + } + private GetBankStatementResponse buildEmptyBankStatementResponse() { GetBankStatementResponse.BankStatementData data = new GetBankStatementResponse.BankStatementData(); data.setTotalCount(0); @@ -420,6 +549,21 @@ class CcdiFileUploadServiceImplTest { return item; } + private MultipartFile createIdCardExcel(String... values) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + List> rows = new ArrayList<>(); + for (String value : values) { + rows.add(List.of(value)); + } + EasyExcel.write(outputStream).sheet("Sheet1").doWrite(rows); + return new MockMultipartFile( + "file", + "id-cards.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + outputStream.toByteArray() + ); + } + private int findEventIndex(List events, String suffix) { for (int i = 0; i < events.size(); i++) { if (events.get(i).endsWith(suffix)) {