合并拉取本行信息后端实现
This commit is contained in:
@@ -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<String> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询上传记录列表
|
||||
*/
|
||||
|
||||
@@ -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<String> idCards;
|
||||
|
||||
/** 开始日期 */
|
||||
private String startDate;
|
||||
|
||||
/** 结束日期 */
|
||||
private String endDate;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String> idCards;
|
||||
|
||||
/** 数量 */
|
||||
private Integer count;
|
||||
}
|
||||
@@ -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<String> parseIdCardFile(MultipartFile file);
|
||||
|
||||
/**
|
||||
* 提交拉取本行信息任务
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param idCards 身份证号列表
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @param userId 当前登录用户ID
|
||||
* @param username 当前登录用户名
|
||||
* @return 批次ID
|
||||
*/
|
||||
String submitPullBankInfo(Long projectId,
|
||||
List<String> idCards,
|
||||
String startDate,
|
||||
String endDate,
|
||||
Long userId,
|
||||
String username);
|
||||
|
||||
/**
|
||||
* 查询上传记录列表
|
||||
*
|
||||
|
||||
@@ -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<String> parseIdCardFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new RuntimeException("身份证文件不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
List<CcdiIdCardExcelRow> rows = EasyExcel.read(file.getInputStream())
|
||||
.head(CcdiIdCardExcelRow.class)
|
||||
.sheet(0)
|
||||
.headRowNumber(1)
|
||||
.doReadSync();
|
||||
|
||||
LinkedHashSet<String> 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<String> 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<CcdiFileUploadRecord> records = new ArrayList<>();
|
||||
List<String> 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<CcdiFileUploadRecord> selectPage(Page<CcdiFileUploadRecord> 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<CcdiFileUploadRecord> records,
|
||||
List<String> 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<String> enterpriseNames = logItem.getEnterpriseNameList();
|
||||
List<String> 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<String> enterpriseNames = logItem.getEnterpriseNameList();
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id">
|
||||
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
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(
|
||||
#{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}
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
@@ -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<SecurityUtils> 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<List<CcdiFileUploadRecord>> inserted = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
List<CcdiFileUploadRecord> 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.<CcdiFileUploadRecord>argThat(item ->
|
||||
"XX身份证.xlsx".equals(item.getFileName())));
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>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.<CcdiFileUploadRecord>argThat(item ->
|
||||
"parsed_failed".equals(item.getFileStatus())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldFailWhenPagedFetchThrows() throws IOException {
|
||||
List<String> 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<List<String>> 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<String> events, String suffix) {
|
||||
for (int i = 0; i < events.size(); i++) {
|
||||
if (events.get(i).endsWith(suffix)) {
|
||||
|
||||
Reference in New Issue
Block a user